Laravel Restful API

Laravel Restful API

Today we are going to build a Laravel 7 API, to book process cinema bookings

API Endpoints Documentation

The end product will look like this

https://documenter.getpostman.com/view/8975783/SzmfYxP4?version=latest

Lets Start

composer create-project laravel/laravel cinema-booking-api

Now edit you .env file

cd book-api
cp .env.example .env

update DB settings in your .env filer

Authentication

Laravel’s laravel/ui package provides a quick way to scaffold all of the routes and views you need for authentication using a few simple commands:

composer require laravel/ui

php artisan ui vue --auth

npm install

npm run dev

Migrations

Add your migrations, NB this app has a pivot table https://laraveldaily.com/pivot-tables-and-many-to-many-relationships/

php artisan make:migration create_customers_table
php artisan make:migration create_movies_table
php artisan make:migration create_showings_table
php artisan make:migration create_customer_showing_table

Then run PHP migrate

php artisan migrate:fresh --seed

Models

Might as well create the Models while I am at it. Please note that there is a Many to many relationships between customers and showings so no need for Bookings model

php artisan make:model Customer
php artisan make:model Movie
php artisan make:model Showing

Customer.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use App\Showing;

class Customer extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email'
    ];

    public function showings()
    {
      return $this->belongsToMany(Showing::class)
            ->withTimestamps()
            ->withPivot('seats');
    }
}

Movie.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Movie extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'movie_name'
    ];

}

Showing.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use App\Movie;

class Showing extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'show_time', 'movie_id'
    ];

    /**
     * Eloquent relationship movie_id FK
     * @return Relationship
     */
    public function movie()
    {
        return $this->belongsTo(Movie::class);
    }
}

JWT

Install jwt, run the following command in your terminal

composer require tymon/jwt-auth

generate a secret key

php artisan jwt:secret
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

You need to implement the JWT contract is the User model

  use Tymon\JWTAuth\Contracts\JWTSubject;

    class User extends Authenticatable implements JWTSubject

In your User model add these two methods

/**
     * getJWTIdentifier.
     * gets identifier 
     * @return 
     */
    public function getJWTIdentifier()
    {
      return $this->getKey();
    }

    /**
     * getJWTCustomClaims.
     *
     * @return 
     */
    public function getJWTCustomClaims()
    {
      return [];
    }
      

Add the below code in config/app.php

'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,

Add this code in Kernel.php

'jwt.auth' => \Tymon\JWTAuth\Http\Middleware\Authenticate::class,
'jwt.refresh' => \Tymon\JWTAuth\Http\Middleware\RefreshToken::class,

Change auth guard to use  jwt guard. Update config/auth.php 

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

Controllers

UserController

php artisan make:controller API/UserController --api
php artisan make:controller API/BookingController --api
php artisan make:controller API/ShowingController --api
php artisan make:controller API/CustomerController --api
php artisan make:controller API/MovieController --api

Validation and Sanitasation

I’m using Waavi/Sanitizer package which has many filters.

Writing validation logic in the controller will break The Single Responsibility Principle. We all know that requirements changes over time and every time requirement get change your class responsibilities is also change. So having many responsibilities in single class make it very difficult to manage.

Laravel has Form Request, A separate request class containing validation logic. To create one you can use below Artisan command.

php artisan make:request MovieStoreRequest
php artisan make:request BookingUpdateRequest
php artisan make:request CustomerStoreRequest
php artisan make:request MovieStoreRequest
php artisan make:request ShowingDeleteRequest
php artisan make:request ShowingStoreRequest
composer require waavi/sanitizer ~1.0

Let’s create BaseFormRequest abstract class for our Form Request and use SanitizesInput trait here.

BaseFromRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\JsonResponse;
use Waavi\Sanitizer\Laravel\SanitizesInput;

abstract class BaseFormRequest extends FormRequest
{
    use SanitizesInput;

    /**
     * For more sanitizer rule check https://github.com/Waavi/Sanitizer
     */
    public function validateResolved()
    {
        {
            $this->sanitize();
            parent::validateResolved();
        }
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    abstract public function rules();

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    abstract public function authorize();
}

The request code is here

UserController Code

<?php

namespace App\Http\Controllers\API;

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

class UserController extends Controller
{

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function register(Request $request)
    {
      //validate incoming request
      $this->validate($request, [
        'name' => 'required|string',
        'email' => 'required|email|unique:users',
        'password' => 'required|confirmed',
    ]);
    try {
        $user = User::create([
          'name' => $request->name,
          'email' => $request->email,
          'password' => bcrypt($request->password),
        ]);

        $token =  auth('api')->login($user);
        return $this->respondWithToken($token);

      } catch (\Exception $e) {
        //return error message
        return response()->json(['message' => 'User Registration Failed!'], 409);
      }
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request)
    {
      $credentials = $request->only(['email', 'password']);
      $token =  auth('api')->attempt($credentials);

      if (!$token) {
        return response()->json(['error' => 'Unauthorized'], 401);
      }

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

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth('api')->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth('api')->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
      return response()->json([
        'access_token' => $token,
        'token_type' => 'bearer',
        'expires_in' => auth('api')->factory()->getTTL() * 60
      ]);
    }
}

The rest of the controller code is here

Booking Service Provider

php artisan make:provider BookingServiceProvider

This will generate a provider class with the same name in the providers directory, copy and replace with the code below

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\CinemaServices\BookingService;

class BookingServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        app()->singleton(BookingService::class, function(){
            return new BookingService;
        });
    }
}

BookingService.php

<?php

namespace App\CinemaServices;

use App\Customer;
use App\Showing;

/**
* Booking Service class to process booking
* Authour Ernest Muroiwa
* 09-May-2020
*/
class BookingService
{
    const MAXSEATS = 10;
    /**
     * processes booking
     *
     * @param  int $user
     * @param  int $showing
     * @param  int $seats
     * @return \Illuminate\Http\Response
     */
    public function booking($customer_id, $showing_id, $seats)
    {
        $customers = Customer::find($customer_id);
        $showings = Showing::find($showing_id);

        $seatDBTotal = $customers->showings->sum('pivot.seats');
        $seatTotal = $seatDBTotal + $seats;
        $seatLeft = self::MAXSEATS - $seatDBTotal;

        // Max of 10 seats can be booked as per requirements
        if ($seatTotal > self::MAXSEATS) {
            return response()->json([
                'success' => false,
                'message' => 'Sorry, only ' . $seatLeft
                . ' seats can be booked at this moment'
            ], 400);
        }

        $customers->showings()->attach($showings, [
                                'seats'=> $seats
                                ]);

        return response()->json([
            'success' => true,
            'message' => 'Seat have been booked'
        ], 200);
    }

    /**
     * Delete booking
     *
     * @param  int $customer
     * @param  int $showing
     * @return \Illuminate\Http\Response
     */
    public function deleteBooking($customer_id, $showing_id)
    {
        $customers = Customer::find($customer_id);

        if (!$customers) {
            return response()->json([
                'success' => false,
                'message' => 'Sorry, customer with id ' . $customer_id
                . ' cannot be found'
            ], 400);
        }

        $customers->showings()->detach($showing_id);

        return response()->json([
            'success' => true
        ]);
    }
}

Register Your Custom Service Provider

So you’ve created your custom service provider. That’s great! Next, you need to inform Laravel about your custom service provider so that it can load it along with other service providers during bootstrapping.

To register your service provider, you just need to add an entry to the array of service providers in the config/app.php file.

BookingController

<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;
use App\Http\Requests\BookingStoreRequest;
use App\Http\Requests\BookingUpdateRequest;
use App\Http\Requests\ShowingDeleteRequest;
use App\Showing;
use App\Customer;

class BookingController extends Controller
{
    /**
     * Store a newly created resource in storage.
     *
     * @param  App\Http\Requests\BookingStoreRequest  $request
     * @return \Illuminate\Http\Response
     */
    public function store(BookingStoreRequest $request)
    {
        //validate
        $validated = $request->validated();

        $booking = resolve('App\CinemaServices\BookingService');
        return $booking->booking($request['customer_id'], $request['showing_id'], $request['number_of_seats']);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  App\Http\Requests\BookingUpdateRequest  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(BookingUpdateRequest $request, $id)
    {
        //validate
        $validated = $request->validated();

        $booking = resolve('App\CinemaServices\BookingService');
        return $booking->booking($id, $request['showing_id'], $request['number_of_seats']);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  App\Http\Requests\ShowingDeleteRequest $request
     * @return \Illuminate\Http\Response
     */
    public function deleteBooking(ShowingDeleteRequest $request)
    {
        $validated = $request->validated();

        $booking = resolve('App\CinemaServices\BookingService');
        return $booking->deleteBooking($request['customer_id'], $request['showing_id']);
    }
}

In your api.php file add the routes.

Route::post('register', 'API\UserController@register');
Route::post('login', 'API\UserController@login');

Route::group(['middleware' => 'jwt.auth'], function () {
    //customer endpoints
    Route::post('customer', 'API\CustomerController@store');
    Route::put('customer', 'API\CustomerController@update');
    Route::delete('customer', 'API\CustomerController@delete');
    //movie endpoints
    Route::post('movie', 'API\MovieController@store');
    Route::put('movie', 'API\MovieController@update');
    Route::delete('movie', 'API\MovieController@delete');
    //showing endpoints
    Route::post('showing', 'API\ShowingController@store');
    Route::put('showing', 'API\ShowingController@update');
    Route::delete('showing', 'API\ShowingController@delete');
    //showing endpoints
    Route::post('booking', 'API\BookingController@store');
    Route::put('booking', 'API\BookingController@update');
    Route::delete('booking', 'API\BookingController@deleteBooking');
});

TDD

Generate Model Factory

We are now ready to generate model factory for our database table. Model Factory will be used to seed our table with test data. We will also make use of Model Factory in our TDD tests.

Model Factory are stored at directory database > factories .  By default, we already have a model factory for User model with name UserFactory.php

Let’s now generate Model Factory for our Customer table.

Run the following command on your terminal at project root directory.

php artisan make:factory CustomerFactory --model=Customer
php artisan make:factory MovieFactory --model=Movie
php artisan make:factory ShowingFactory --model=Showing
php artisan make:test ShowingTest
php artisan make:test BookingTest
php artisan make:test CustomerTest
php artisan make:test MovieTest

Tests are found here and factories here

So, to toggle this in tests you can either set it in the .env.testing file or in the phpunit.xml file:

# phpunit.xml

<env name="DB_FOREIGN_KEYS" value="false"/>

Database Seeders

php artisan make:seeder CreateUserSeeder
php artisan make:seeder CreateCustomerSeeder
php artisan make:seeder CreateMovieSeeder
php artisan make:seeder CreateShowingSeeder
php artisan make:seeder CreateBookingSeeder

Seeders are found here

Run this command in the terminal to migrate and seed the DB

php artisan migrate:fresh --seed

Leave a Reply

Close Menu