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