Build, Test and Document REST APIs in PHP & Laravel — Part I — Building
Co-Authored by Regina Sharon G, Santhosh Krishnamoorthy, Swati Akkishetti, Charan Sai & Harsha Sudheer
In this 3 part series, you will be learning how to create RESTful CRUD APIs in PHP & Laravel, Test and Document them using Swagger.
The APIs we will be building will be for a Book Shop. With these APIs, users will be able to Create, Read, Update & Delete books in the Shop.
We will look at how to build these APIs in this instalment. In the subsequent instalments, we will delve into testing and documenting them.
Prerequisites
- PHP 7.4.24 or Higher
- PHP Composer
- Laravel 8.0 or Higher
- Postman (optional)
To follow along with this tutorial, a basic understanding of the PHP language and Laravel framework is required.
Creating a Laravel Project
To get started, you have to create a Laravel application. To do this you have to run the following command in your terminal:
composer create-project laravel/laravel BookShop
Next, change your current directory to the root folder of the project:
cd BookShop
Start up the Laravel server if it’s not already running:
php artisan serve
You will be able to visit your application on http://127.0.0.1:8000.
If you see this, then all is good, you are ready to move on.
Set Up Database Connection
As we will be using SQLITE as our database for storing books related data, let us configure it in our Laravel application.
Open the .env
file in the project folder and update the DB_CONNECTION
value to sqlite as below:
DB_CONNECTION=sqlite
Navigate to the BookShop/database
folder and create an empty file and name it as database.sqlite
.
Setting Up Migrations and Models
With the database configured let us now create the models and database migration files. To do this you have to run:
php artisan make:model Author -m
php artisan make:model Book -m
New files named Authors.php
, Books.php
will be created in the app/Models
directory.
Additionally, migration files will be created in the database/migrations
directory to generate our table. You will have to modify the migration files to create a columns for both Books & Author tables.
Open <timestamp>_create_authors_table.php
file in the database/migration directory and replace the up()
function with the code below:
...
public function up()
{
Schema::create('authors', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('first_name');
$table->string('last_name');
$table->string('email');
});
}
...
Open <timestamp>_create_books_table.php
file in the database/migration directory and replace the up()
function with the code below:
...
public function up()
{
Schema::create('books', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->integer('price');
$table->unsignedBigInteger('author_id');
$table->foreign('author_id')->references('id')- >on('authors');
});
}
...
Run the following command to apply migrations:
$php artisan migrate
Verify database tables
- To visualise the table and data available in SQLITE file (database.sqlite), we need a tool so let us download and install DB Browser for SQLite
- Navigate to the path app/database folder in the project and open database.sqlite with DB Browser for SQLite to view tables and data.
Setting up the Routes
With the database all set up, the next step would be to configure the routes. Routes are basically the HTTP URL locations at which the API endpoints will be available.
Before we do that, let us change the base path for our API urls.
By default, Laravel has a prefix /api/
set for the API urls. This means, your API endpoints will be at http://localhost:8000/api/books
. If you prefer to have them at ‘/books’ instead, make the changes in RouteServiceProvider.php
file in app/providers/
...
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
}
...
Defining the Routes
For our Book Shop application we will need routes for Adding, Updating, Getting and Deleting a book.
Proceed to the routes
directory and open the api.php
file and create the endpoints as below:
Route::get('/books/{id}', [BookController::class, 'getBook']);
Route::post('/books', [BookController::class, 'addBook']);
Route::put('/books/{id}', [BookController::class, 'updateBook']);
Route::delete('/books/{id}', [BookController::class, 'deleteBook']);
Also, run the following command to verify that the routes are added in properly
php artisan route:list
Note: At this point, BookController
is not yet created. Let us create that in the below sections
A look at the project’s folder structure :
Following the common pattern used in application development.
Controllers — contains application logic and passing user input data to service
Services — The middleware between controller and repository. Gather data from controllers, perform validation and business logic, and call repositories for data manipulation.
Repositories — layer for interaction with models and performing DB operations
Models — common laravel model files with relationships defined with database tables
Let’s say, each layer interacts with only single layer at a point
for each DB table there is a model
for each model there is a repository
for each repository there is a service
for each service there is a controller
So basically all layers are 1:1 to each other.
To give example for our case,
BookController -> BookService -> BookRepository -> BookModel
Implement the End points :
Let us now start adding code. Firstly, we shall implement for the ‘Add Book’ endpoint.
Create Repository
Now, let us create the following interface and class files in the Repository folder.
IBookRepository
php interface file inapp/Repository/IBookRepository.php
BookRepository
php class inapp/Repository/BookRepository.php
IAuthorRepository
php interface file inapp/Repository/IAuthorRepository.php
AuthorRepository
php class inapp/Repository/IAuthorRepository.php
IBookRepository:
Declare addBook() method which contains $bookDetails as input parameter and return type as boolean
function addBook($bookDetails): bool;
BookRepository:
Implement addBook() method in BookRepository class as below:
public function addBook($bookDetails): bool
{
return Books::query()->insert($bookDetails);
}
IAuthorRepository:
- Declare
getAuthorIdFromAuthorName()
method which contains$authorName
as input parameter and return type as integer. - Declare
addAuthor()
method which contains$author
details as input parameter and return type as integer which is author id.
function getAuthorIdFromAuthorName($authorName);function addAuthor($author);
AuthorRepository:
- Implement
getAuthorIdFromAuthorName()
method inAuthorRepository
class. - Implement
addAuthor()
method inAuthorRepository
class.
public function getAuthorIdFromAuthorName($authorName)
{
return Authors::query()->where('authors.first_name', '=', $authorName)->first('id');
}public function addAuthor($author): int
{
return Authors::query()->insertGetId($author);
}
Register application classes
After creating the interface and class of the repository layer , let us bind them in app/Providers/AppServiceProvider.php
...
public function register()
{
$this->app->bind(IBookRepository::class, function (){
return new BookRepository();
});
$this->app->bind(IAuthorRepository::class, function (){
return new AuthorRepository();
});
}
...
Create Service
Next, let us create a service class BookService.php
under service folder app/Service
Create the constructor in the service layer(BookService.php
) and pass both the repository layers(IBookRepository, IAuthorRepository
), as constructor parameters.
private $bookRepository;
private $authorRepository;
public function __construct(IBookRepository $bookRepository, IAuthorRepository $authorRepository)
{
$this->bookRepository = $bookRepository;
$this->authorRepository = $authorRepository;
}
Add the business logic to a service class by creating a new method addBook()
.
public function addBook($title, $price, $author): string
{
$authorDetails = $this->authorRepository->getAuthorIdFromAuthorName($author['first_name']);
if ($authorDetails == null) {
$authorId =$this->authorRepository->addAuthor($author);
$bookDetails =array('title' => $title, 'price' => $price, 'author_id' => $authorId );
}else{
$bookDetails = array('title' => $title, 'price' => $price, 'author_id' => $authorDetails->id);
}
if ($this->bookRepository->addBook($bookDetails)) {
return "Book is added successfully";
}
return "We are not able to add a book";
}
In the request body, the author details are sent. But we are saving author id in our Book table. So first we have to get the author’s id from author details and then pass the authorId
as params.
Create Controller
Next, let us generate a BookController
. For that run the below command to create a new controller in app/Http/Controllers
.
php artisan make:controller BookController
You will find a new file named BookController.php
in the app\Http\Controllers
directory.
Create the constructor in the Controller layer(BookController) and pass the service layer(BookService), as a constructor parameter.
private $bookService;
public function __construct(BookService $bookService)
{
$this->bookService = $bookService;
}
Let us write our logic in the addBook() method in the controller where we will be sending the request body as parameters for the service layer(BookService.php) and returning the response as JSON.
public function addBook(Request $request): JsonResponse
{
$bookAdded = $this->bookService->addBook($request->all()["title"], $request->all()["price"], $request->all()["author"]);
return response()->json($bookAdded, 201);
}
Verifying the Endpoints
Before testing, make sure your application is running. You can use the inbuilt command as mentioned earlier:
php artisan serve
To test this endpoint open Postman and make a POST request to http://127.0.0.1:8000/books
Implementing GET, Update and Delete endpoints
We can implement these by using a similar approach as we did for the AddBook API.
app/Repository/IBookRepository.php
function getBookDetails($bookId);
function deleteBook($bookId): int;
function updateBook($bookId,$title , $price): int;
app/Repository/BookRepository.php
public function getBookDetails($bookId)
{
return Books::query()->where('books.id', '=', $bookId)->first();
}
public function deleteBook($bookId): int
{
return Books::query()->where('books.id', '=', $bookId)->delete();
}public function updateBook($bookId,$title, $price): int
{
return DB::table('books')->where('books.id', '=', $bookId)->update(['books.title' => $title,'books.price' => $price]);
}
app/Repository/IAuthorRepository.php
function getAuthorDetailsFromAuthorId($authorId);
app/Repository/AuthorRepository.php
public function getAuthorDetailsFromAuthorId($authorId)
{
return Authors::query()->where('authors.id', '=', $authorId)->first(['first_name','last_name','email']);
}
app/Service/BookService.php
public function getBookDetails($bookId): array
{
$bookDetails = $this->bookRepository->getBookDetails($bookId);
if ($bookDetails == null) {
return [];
}
$authorDetails = $this->authorRepository->getAuthorDetailsFromAuthorId($bookDetails->author_id);
return array("title" => $bookDetails->title, "price" => (int)$bookDetails->price, "author" => $authorDetails);
}public function deleteBook($bookId): string
{
$isDeleted = $this->bookRepository->deleteBook($bookId);
if ($isDeleted == 1) {
return "Book is deleted successfully";
}
return "Book is not present";
}public function updateBook($bookId, $title, $price): string
{
if ($this->bookRepository->updateBook($bookId, $title, $price) == 1) {
return "Book is updated successfully";
}
return "Book is not available";
}
app/Http/Controllers/BookController.php
public function getBook($bookId): JsonResponse
{
$bookDetails = $this->bookService->getBookDetails($bookId);
return response()->json($bookDetails);
}public function deleteBook($bookId): JsonResponse
{
$isDeleted = $this->bookService->deleteBook($bookId);
return response()->json($isDeleted);
}public function updateBook($bookId, Request $request): JsonResponse
{
$bookUpdated = $this->bookService->updateBook($bookId ,$request->all()["title"] , $request->all()["price"]);
return response()->json($bookUpdated);
}
Verifying the Endpoints
GetBook details
UpdateBook details
DeleteBook
With that, we have successfully built the REST APIs for our hypothetical BookShop application. In the subsequent instalments of this series, we will be looking into testing and documenting them.
Part II- Testing
Part III- Documenting