Hire the author: Stephen I

Introduction

My name is Ilori Stephen Adejuwon and I am a Fullstack software developer (Backend Heavy) based in West Africa Lagos, Nigeria. In this tutorial, I will show you how to build a REST API in PHP.

But before we proceed, I know you are curious to know what the acronym REST and API means. Well, we will cover a lot on those two in this Article. Meanwhile, let’s talk about why you would ever want to build a REST API first.

However, you can download the project from Git by visiting PHP Rest API.

Why Would You Want To Build A REST API.

PHP is a server-side language and it’s perfect for doing a lot of server-side tasks such as Receiving, Handling HTTP RequestsAuthenticationcommunicating with a Database. and building an API.

That doesn’t still explain why you want to build a REST API. Above all, Well you see, you’d want to build a REST API because it gives room for two applications to talk to each other.

Now I bet you have a lot of questions flowing through your mind. Although I might not be able to answer all of those questions but I believe you will find answers once you continue reading.

Super excited huh? Let’s talk about some terms or technical jargon related to this lesson.

Glossary

While preparing this project, I came across a whole lot of terms or technical jargon related to this topic. Therefore in order not to get you confused, I will do my very best to explain each of them.

  1. REST: The term REST also known as REPRESENTATIONAL STATE TRANSFER can be defined as a service that defines a set of functions that programmers can use to send requests and receive responses using HTTP protocol such as GET and POST.

  2. API: The term API which stands for APPLICATION PROGRAMMING INTERFACE, and it’s a software intermediary that allows two applications to talk to each other.

  3. HTTP VERBS: This actually means your HTTP PROTOCOL such as your GETPOSTPUT and PATCH requests.

  4. ENDPOINTS: The term endpoint in the simplest form is one end of a communication channel. When an API interacts with another system, the touchpoints of this communication are considered endpoints. Each endpoint is the location from which APIs can access the resources they need to carry out their function.

  5. Middleware: This is a service that exists in between your application making it so that you focus on the specific purpose of your application.

In conclusion, I believe we are on the same page now. Therefore, let’s take our time to talk about the Project Requirement next.

Project Requirements.

  1. A Localhost Server: You can download the latest version of any of the following. XAMPP, LAMPP, MAMPP, and AMPPS depending on your operating system.

  2. Composer: Composer is a package manager for PHP. This will help us install some external packages which we are going to use in building our REST API. You can download and install composer via Get Composer.

  3. Git: This is kinda optional but I still recommend having git installed. This will prove useful if you ever want to deploy your code or push to a remote repository. You can download and install git through Git.

  4. PHP: Yup! PHP. I also think it’s best to have at least Basic PHP Knowledge. The coding is really easy. Nothing scary but I still recommend you know the basics of PHP to continue.

  5. Postman: We will need Postman to consume our Api’s. This is just for testing our Api locally. It provides a whole lot of extra features but for now, just download Postman.

  6. Text Editor: We will need a text editor to write our codes with. You can go online and check out any text editors but I recommend atom or visual studio code.

  7. Determination: I am not a motivational speaker but if you are a beginner, you shouldn’t get intimated by the sound of this topic. In addition, it’s something really easy and I will do my best to break it down. But still, I recommend that you encourage yourself.

In short, that’s our project requirement. Let’s talk about our project directory and begin Hacking!

Project Directory.

*/ PHP-REST-API
*/ App (Our Application Logic)
*/ public (Our public directory)
composer.json
index.php (Root Directory)

Isn’t it beautiful? That’s our project directory. A parent folder called php-rest-api and two other subfolders called App and public.

With that setup, open the composer.json File in the project’s root directory and paste in the code snippet below. I will explain the contents later.

  {
  "require": {
  "klein/klein": "^2.1",
  "firebase/php-jwt": "^5.2"
  },
  "autoload": {
  "psr-4": {
  "App\": "App/"
  },
  "classmap": [
  "App/Controller",
  "App/Middleware",
  "App/Model"
  ]
  }
  }
view rawpackage.json hosted with ❤ by GitHub

Inside of the composer.json File, we have a require Object. This object holds all our project dependencies or external packages, whichever clause you are comfortable with.

We also have an autoload Object. This Object enables us to use namespaces in our project and it also helps in autoloading our classes.

With that said, you can now open the project up in your terminal or cmd and run the command composer install. This will install the Klein package and a Firebase JWT package.

This will also create a vendor folder. This is where the installed packages will live with a few composer configurations and lastly, it will generate/create a new composer.lock file.

In short, We just installed some packages needed for our application. Therefore, Let’s move on and edit the index.php file inside of our project’s root directory.

1. Let’s Edit Our Index.php (Root Directory).

Our index.php will serve as an Entry point into the application. This acts as the file that starts the application. It doesn’t contain much though. You can open your index.php file and paste the snippet below.

  <?php
  /**
  * @author Ilori Stephen A
  **/
  require_once __DIR__ . '/vendor/autoload.php';
  require_once __DIR__ . '/App/routes/api.php';
   
  ?>
view rawindex.php hosted with ❤ by GitHub

The index.php does nothing other than autoloading all of our packages and also the router which contains all of our API Endpoints. And it does all of that in just two lines! That’s the beauty of composer and namespaces.

2. Creating The Endpoints (App/routes Directory).

With our index.php setup, If you try to run the application, You might find some errors. Well, that’s because we are yet to create our api.php file.

The api.php file is where all of our REST API Endpoints are defined. With that said, create an api.php file inside of the routes folder in the App directory. Once done, paste the code snippet below into the api.php file.

  <?php
  namespace App;
  use App\UserController;
  use App\CatalogController;
  use App\ProductController;
   
  $Klein = new \Klein\Klein();
   
  /******************** User Routes || Authentication Routes **********************/
  $Klein->respond('POST', '/api/v1/user', [ new UserController(), 'createNewUser' ]);
  $Klein->respond('POST', '/api/v1/user-auth', [ new UserController(), 'login' ]);
   
  /******************** Catalog Routes **********************/
  $Klein->respond('POST', '/api/v1/catalog', [ new CatalogController(), 'createNewCatalog' ]);
  $Klein->respond(['PATCH', 'PUT'], '/api/v1/catalog/[:id]', [ new CatalogController(), 'updateCatalog']);
  $Klein->respond(['GET', 'HEAD'], '/api/v1/fetch-catalog-by-id/[:id]', [ new CatalogController(), 'fetchCatalogById' ]);
  $Klein->respond(['GET', 'HEAD'], '/api/v1/fetch-catalog-by-name/[:name]', [ new CatalogController(), 'fetchCatalogByName' ]);
  $Klein->respond(['GET', 'HEAD'], '/api/v1/catalogs', [ new CatalogController(), 'fetchCatalogs' ]);
  $Klein->respond('DELETE', '/api/v1/del-catalog/[:id]', [ new CatalogController(), 'deleteCatalog' ]);
   
  /******************** Product Routes **********************/
  $Klein->respond('POST', '/api/v1/product', [ new ProductController(), 'createProduct' ]);
  $Klein->respond('POST', '/api/v1/product/[:id]', [ new ProductController(), 'updateProduct' ]);
  $Klein->respond('GET', '/api/v1/fetch/[:id]', [ new ProductController(), 'getProductById' ]);
  $Klein->respond('GET', '/api/v1/products', [ new ProductController(), 'fetchProducts' ]);
  $Klein->respond('DELETE', '/api/v1/delete-product/[:id]', [ new ProductController(), 'deleteProduct' ]);
   
  // Dispatch all routes....
  $Klein->dispatch();
   
  ?>
view rawapi.php hosted with ❤ by GitHub

Now, at the top level of the api.php file, we declared a namespace. This namespace makes it possible to use any functions, Classes, or Constants defined within the namespace App.

With its definition at the top level of our script, we can begin using other classes by issuing the keyword use with the namespace path App\ClassName‘ to the class.

You have to admit, this is a lot better than using the require keyword or function. Above all, it makes our code look more modern and neat.

Because we required the autoload installed from issuing the composer install command in our index.php, this makes it easy for us to use any of the installed composer packages.

Meanwhile, We created a few Endpoints by creating a new instance of the Klein Package installed via composer. You can create a simple Endpoint with the Klein Package by following the syntax below.

    $Klein-&gt;respond('HTTP VERB', 'DESIRED_URL', CALLBACK_FUNCTION);

Similarly, you can read more about the Klein Package by visiting their Github Repo. Therefore, let’s continue by creating some Models for our application.

3. Spinning Up The Base Model (App\Model Directory).

It’s an API but still, we need a database to store some Data. Let’s begin by creating a Model.php file inside of the Model folder in the App directory.

This acts as the Base Model for every model file in the Model folder. That is, every other Model must extend or require this Class or File. This Class is also declared within the App namespace.

The Model.php Class creates a new Database Connection, and share a few private methods for handling some Database operations. With that completed, create a Model.php file inside of the Model folder in the App Directory and paste the code snippet below.

  <?php
  namespace App;
   
  use PDO;
  use Exception;
   
  /**
  * Model - The Base Model for all other Models.... All Other Model extends this Model.
  *
  * @author Ilori Stephen A <stephenilori458@gmail.com>
  * @link https://github.com/learningdollars/php-rest-api/App/Model/Model.php
  * @license MIT
  */
  class Model {
  protected static $dbHost = '127.0.0.1';
  protected static $dbName = 'php_mini_rest_api';
  protected static $dbUser = 'root';
  protected static $dbPass = '';
  protected static $dbConn;
  protected static $stmt;
   
  /**
  * __construct
  *
  * Creates a New Database Connection...
  *
  * @param void
  * @return void
  */
  public function __construct()
  {
  // Create a DSN...
  $Dsn = "mysql:host=" . Self::$dbHost . ";dbname=" . Self::$dbName;
  $options = array(
  PDO::ATTR_PERSISTENT => true,
  PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
  );
   
  try {
  Self::$dbConn = new PDO($Dsn, Self::$dbUser, Self::$dbPass, $options);
  } catch(Exception $e) {
  $Response = array(
  status => 500,
  data => [],
  message => $e->getMessage()
  );
  return $Response;
  }
  }
   
  /**
  * query
  *
  * Takes advantage of PDO prepare method to create a prepared statement.
  *
  * @param string $query Sql query from extending Models
  * @return void Anonymos
  */
  protected static function query($query)
  {
  Self::$stmt = Self::$dbConn->prepare($query);
  return true;
  }
   
  /**
  * bindParams
  *
  * Binds the prepared statement using the bindValue method.
  *
  * @param mixed $param, $value, $type The parameter to bind the value to and the data type which is by default null.
  * @return void Anonymos
  */
  protected static function bindParams($param, $value, $type = null)
  {
  if ($type == null) {
  switch(true) {
  case is_int($value):
  $type = PDO::PARAM_INT;
  break;
  case is_bool($value):
  $type = PDO::PARAM_BOOL;
  break;
  case is_null($value):
  $type = PDO::PARAM_NULL;
  break;
  default:
  $type = PDO::PARAM_STR;
  break;
  }
  }
   
  Self::$stmt->bindValue($param, $value, $type);
  return;
  }
   
  /**
  * execute
  *
  * Executes the Sql statement and returns a boolean status
  *
  * @param void
  * @return boolean Anonymos
  */
  protected static function execute()
  {
  Self::$stmt->execute();
  return true;
  }
   
  /**
  * fetch
  *
  * Executes the Sql statement and returns a single array from the resulting Sql query.
  *
  * @param void
  * @return array Anonymos
  */
  protected static function fetch()
  {
  Self::execute();
  return Self::$stmt->fetch(PDO::FETCH_ASSOC);
  }
   
  /**
  * fetchAll
  *
  * Executes the Sql statement and returns an array from the resulting Sql query.
  *
  * @param void
  * @return array Anonymos
  */
  protected static function fetchAll()
  {
  Self::execute();
  return Self::$stmt->fetchAll(PDO::FETCH_ASSOC);
  }
   
  /**
  * lastInsertedId
  *
  * Makes use of the database connection and returns the last inserted id in the database.
  *
  * @param void
  * @return int Anonymos
  */
  protected static function lastInsertedId()
  {
  return Self::$dbConn->lastInsertId();
  }
  }
  ?>
view rawModel.php hosted with ❤ by GitHub

You can replace the Model Class Properties with your own Environment Variables. However the $stmt and the $dbConn should be untouched.

I won’t spend much time explaining what this file does as I have explained how this file works in a different article. In conclusion, you can read How To Create A Login And System In PHP Using PDO in order to get the full list.

In short, I will tell you that this class creates a new PDO Connection and it also provides an abstraction layer by hiding some business logic and exposing some reusable Method.

Meanwhile, I think it’s time to create other Models. In the next line, we will talk about the UserModel.

4. Coding The UserModel (App\Model Directory).

UserModel.php file is the Model which is responsible for creating, reading, updating and deleting users. This Model extends the Base Model making it possible to use Methods created or owned by the Base Model.

Create a UserModel.php file inside the Model Folder within the App Directory that should look like the following.

  <?php
  namespace App;
  use App\Model;
   
  /**
  * UserModel - This Model is consumed basically by the UserController and is also consumed by other controllers and Middlewares...
  *
  * @author Ilori Stephen A <stephenilori458@gmail.com>
  * @link https://github.com/learningdollars/php-rest-api/App/Model/UserModel.php
  * @license MIT
  */
  class UserModel extends Model {
   
  /**
  * createUser
  *
  * creates a new User
  *
  * @param array $payload Contains all the fields that will be created.
  * @return array Anonymos
  */
  public static function createUser($payload)
  {
  $Sql = "INSERT INTO `db_users` (firstName, lastName, email, password, created_at, updated_at) VALUES (:firstName, :lastName, :email, :password, :created_at, :updated_at)";
  Parent::query($Sql);
  // Bind Params...
  Parent::bindParams('firstName', $payload['firstName']);
  Parent::bindParams('lastName', $payload['lastName']);
  Parent::bindParams('email', $payload['email']);
  Parent::bindParams('password', $payload['password']);
  Parent::bindParams('created_at', $payload['created_at']);
  Parent::bindParams('updated_at', $payload['updated_at']);
   
  $newUser = Parent::execute();
  if ($newUser) {
  $user_id = Parent::lastInsertedId();
  $payload['user_id'] = $user_id;
  $Response = array(
  'status' => true,
  'data' => $payload
  );
  return $Response;
  }
   
  $Response = array(
  'status' => false,
  'data' => []
  );
  return $Response;
  }
   
  /**
  * fetchUserById
  *
  * fetches a user by it's Id
  *
  * @param int $Id The Id of the row to be fetched...
  * @return array Anonymos
  */
  public static function fetchUserById($Id)
  {
  $Sql = "SELECT id, firstName, lastName, email, created_at, updated_at FROM `db_users` WHERE id = :id";
  Parent::query($Sql);
  // Bind Params...
  Parent::bindParams('id', $Id);
  $Data = Parent::fetch();
   
  if (!empty($Data)) {
  return array(
  'status' => true,
  'data' => $Data
  );
  }
   
  return array(
  'status' => false,
  'data' => []
  );
  }
   
  /**
  * checkEmail
  *
  * fetches a user by it's email
  *
  * @param string $email The email of the row to be fetched...
  * @return array Anonymos
  */
  public static function checkEmail($email)
  {
  $Sql = "SELECT * FROM `db_users` WHERE email = :email";
  Parent::query($Sql);
  // Bind Params...
  Parent::bindParams('email', $email);
  $emailData = Parent::fetch();
  if (empty($emailData)) {
  $Response = array(
  'status' => false,
  'data' => []
  );
  return $Response;
  }
   
  $Response = array(
  'status' => true,
  'data' => $emailData
  );
  return $Response;
  }
  }
  ?>

Inside of the UserModel Class, we have the createUser Static Method which makes it possible to create a new User record in the Database by reusing methods like querybind params and execute from the Base Model Class.

This Method accepts an Array of User Information and it returns a new Array containing the status of the operation.

Inside of this class, we also have the fetchUserById Static Method which makes it possible to fetch a User by ID. This method accepts and Integer and it returns an Array containing the status of the operation.

And last but not least, we have the checkEmail Method which fetches a User based on his/her Email Address. This method accepts an Email String and it returns an Array containing the status of the operation.

In conclusion, we have successfully created our UserModel. Later on, this model will prove useful within the Controllers and Middlewares. It’s time to shift our focus to the next Model we will be creating. The TokenModel.

5. The TokenModel (App\Model Directory).

TokenModel.php File or Class inherits the Base Model Class and it’s responsible for storing Access Tokens that can be used for communicating with this API.

You can think of the Access Tokens as a required Key which is needed to communicate with the API. You pass an Access Token to the API, it checks the Token if it’s valid and then, it proceeds with your request. If the validation goes sideways, The request will fail.

create a TokenModel.php file inside the Model folder in the App directory with the code snippet below.

  <?php
  namespace App;
  use App\Model;
   
  /**
  * TokenModel - This Model is consumed basically by the UserController and is also consumed by other controllers and Middlewares...
  *
  * @author Ilori Stephen A <stephenilori458@gmail.com>
  * @link https://github.com/learningdollars/php-rest-api/App/Model/TokenModel.php
  * @license MIT
  */
  class TokenModel extends Model {
   
  /**
  * createToken
  *
  * creates a new Token
  *
  * @param array $payload Contains all the fields that will be created.
  * @return array Anonymous
  */
  public function createToken($payload)
  {
  $Sql = "INSERT INTO db_token (user_id, jwt_token) VALUES (:user_id, :jwt_token)";
  Parent::query($Sql);
  // Bind Params...
  Parent::bindParams('user_id', $payload['user_id']);
  Parent::bindParams('jwt_token', $payload['jwt_token']);
   
  $Token = Parent::execute();
  if ($Token) {
  return array(
  'status' => true,
  'data' => $payload
  );
  }
   
  return array(
  'status' => false,
  'data' => []
  );
  }
   
  /**
  * fetchToken
  *
  * fetches an existing Token using the $token
  *
  * @param string $token The token that will be used in matching the closest token from the database.
  * @return array Anonymous
  */
  public function fetchToken($token)
  {
  $Sql = "SELECT * FROM `db_token` WHERE jwt_token = :jwt_token";
  Parent::query($Sql);
  Parent::bindParams('jwt_token', $token);
   
  $Data = Parent::fetch();
  if (!empty($Data)) {
  return array(
  'status' => true,
  'data' => $Data
  );
  }
   
  return array(
  'status' => false,
  'data' => []
  );
  }
  }
  ?>

TokenModel Class houses two Methods. The createToken Method and the fetchToken Method.

createToken Method accepts an Array and returns an Array containing the result of the Insert Operation by taking advantage of the Base Model methods.

fetchToken Method accepts a String and returns the first record that matches the token String in the Database. This operation returns an Array.

To sum up, let’s move on and create the CatalogModel next.

6. Model For Catalogs (App\Model Directory).

UserModel.php File or Class is responsible for creating, updating, fetching, and deleting Catalogs. This File extends the Base Model just like the previous Models. Create a new file inside of the Model Folder and name it CatalogModel.php.

Open the CatalogModel.php and after that, paste the code snippet below into it.

  <?php
  namespace App;
   
  use App\Model;
   
  /**
  * CatalogModel - A Model for the Catalog Controller.
  *
  * @author Ilori Stephen A <stephenilori458@gmail.com>
  * @link https://github.com/learningdollars/php-rest-api/App/Model/CatalogModel.php
  * @license MIT
  */
  class CatalogModel extends Model {
   
  /**
  * createCatalog
  *
  * Creates a New Catalog
  *
  * @param array $Payload Contains all the required data needed to create a Catalog.
  * @return array Anonymous
  */
  public static function createCatalog($Payload)
  {
  $Sql = "INSERT INTO `db_catalogs` (name, created_at, updated_at) VALUES (:name, :created_at, :updated_at)";
  Parent::query($Sql);
   
  Parent::bindParams('name', $Payload['name']);
  Parent::bindParams('created_at', $Payload['created_at']);
  Parent::bindParams('updated_at', $Payload['updated_at']);
   
  $catalog = Parent::execute();
  if ($catalog) {
  $catalog_id = Parent::lastInsertedId();
  $Payload['catalog_id'] = $catalog_id;
  return array(
  'status' => true,
  'data' => $Payload,
  );
  }
   
  return array(
  'status' => false,
  'data' => []
  );
  }
   
  /**
  * updateCatalog
  *
  * Updates a New Catalog
  *
  * @param array $Payload Contains all the fields that will be updated.
  * @return array Anonymous
  */
  public static function updateCatalog($Payload)
  {
  $Sql = "UPDATE `db_catalogs` SET name = :name, updated_at = :updated_at WHERE id = :id";
  Parent::query($Sql);
   
  Parent::bindParams('id', $Payload['id']);
  Parent::bindParams('name', $Payload['name']);
  Parent::bindParams('updated_at', $Payload['updated_at']);
   
  $catalog = Parent::execute();
  if ($catalog) {
  return array(
  'status' => true,
  'data' => $Payload,
  );
  }
   
  return array(
  'status' => false,
  'data' => []
  );
  }
   
  /**
  * fetchCatalogByID
  *
  * Returns the first Catalog that matches the ID
  *
  * @param int $Id The Id of the Row to be updated.
  * @return array Anonymous
  */
  public static function fetchCatalogByID($Id)
  {
  $Sql = "SELECT * FROM `db_catalogs` WHERE id = :id";
  Parent::query($Sql);
   
  Parent::bindParams('id', $Id);
  $catalog = Parent::fetch();