Build a Simple REST API in PHP
Tags: php, rest, api
Published: 2020-12-21 21:09:50

**RE**presentational **S**tate **T**ransfer (REST) is a software architectural style that defines a set of constraints to be used for creating Web Services. REST APIs are one of the pillars of modern web development. Most web applications these days are developed as single-page applications on the frontend, connected to backend APIs written in different languages. There are various PHP frameworks which could help you build a REST API in minutes. But, let us learn how to build a simple REST API in core PHP.

This is the first part of the `Learn PHP` series, where you'll learn how to build a REST API in PHP using core PHP.

Next two:

- Secure your API with [Magic](https://magic.link/).
  {% post magiclabs/secure-your-php-rest-api-with-magic-82k %}
- Deploy your API to [Heroku](https://heroku.com/).
  {% post shahbaz17/deploy-your-php-api-to-heroku-4lkj %}

### Prerequisites

- [PHP](https://www.php.net/downloads.php)
- [MySQL](https://www.mysql.com/downloads/)
- [Composer](http://getcomposer.org/)
- [Postman](https://www.postman.com/downloads/)

# PHP REST API Skeleton

Create a `/src` directory, and a `composer.json` file in the top directory with one dependency: the [DotEnv](https://github.com/vlucas/phpdotenv) library, which allows storing secured pieces of information in `.env` file.

`composer.json`

```json
{
  "require": {
    "vlucas/phpdotenv": "^5.3"
  },
  "autoload": {
    "psr-4": {
      "Src\\": "src/"
    }
  }
}
```

A PSR-4 autoloader will automatically look for PHP classes in the `/src` directory.

Install dependencies now:

```bash
composer install
```

It will create a `/vendor` directory, and the DotEnv dependency will be installed (autoloader will load classes from `/src` with no `include()` calls).

Create a `.gitignore` file for your project with two lines in it, so the `/vendor` directory and local `.env` file will be ignored:

`.gitignore`

```
vendor/
.env
```

Next, create a `.env.example` file for Secret variables:

`.env.example`

```php
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
```

and a `.env` file where you'll fill in your actual details later (it will be ignored by Git so it won’t end up in your repository).

Create a `start.php` file which loads the environment variables.

`start.php`

```php
<?php
require 'vendor/autoload.php';

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();

echo $_ENV['DB_HOST'];

// test code:
// it will output: localhost
// when you run $ php start.php
```

# Configure the Database for your PHP REST API

We will use MySQL to power our simple API.
Create a new database and user for your app:

```bash
mysql -u root -p
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'rest_api_user'@'localhost' identified by 'rest_api_password';
GRANT ALL on blog.* to 'rest_api_user'@'localhost';
quit
```

The REST API will contain posts for our `Blog` Application, with the following fields: `id`, `title`, `body`, `author`, `author_picture`, `created_at`. It allows users to post their blog on our `Blog` application.

Create the database table in MySQL.

```php
mysql -u rest_api_user -p;
// Enter your password
use blog;

CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `body` text NOT NULL,
  `author` varchar(255),
  `author_picture` varchar(255),
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
);

```

Add the database connection variables to your `.env` file:

`.env`

```php
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=rest_api_user
DB_PASSWORD=rest_api_password
```

Create a class to hold the database connections and add the initialization of the connection to the `start.php` file.

`src/Database.php`

```php
<?php
namespace Src;

class Database {

  private $dbConnection = null;

  public function __construct()
  {
    $host = $_ENV['DB_HOST'];
    $port = $_ENV['DB_PORT'];
    $db   = $_ENV['DB_DATABASE'];
    $user = $_ENV['DB_USERNAME'];
    $pass = $_ENV['DB_PASSWORD'];

    try {
      $this->dbConnection = new \PDO(
          "mysql:host=$host;port=$port;dbname=$db",
          $user,
          $pass
      );
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }

  public function connet()
  {
    return $this->dbConnection;
  }
}
```

`start.php`

```php
<?php
require 'vendor/autoload.php';

use Src\Database;

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();

$dbConnection = (new Database())->connet();
```

# Add a Class for the Post Table and Implement PHP REST API

There are many ways to interact with the database in an object-oriented context, but, let us go with a simple method where you will implement methods to return all posts, return a specific post and add/update/delete a post.

Also, the API endpoints which will be handled by our frontend `api/index.php`.

REST API with the following endpoints:

## Your APIs

| API               |    CRUD    |                                Description |
| :---------------- | :--------: | -----------------------------------------: |
| GET /posts        |  **READ**  |        Get all the Posts from `post` table |
| GET /post/{id}    |  **READ**  |        Get a single Post from `post` table |
| POST /post        | **CREATE** | Create a Post and insert into `post` table |
| PUT /post/{id}    | **UPDATE** |            Update the Post in `post` table |
| DELETE /post/{id} | **DELETE** |            Delete a Post from `post` table |

`api/index.php`

```php
<?php
require "../start.php";
use Src\Post;

header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode( '/', $uri );

// endpoints starting with `/post` or `/posts` for GET shows all posts
// everything else results in a 404 Not Found
if ($uri[1] !== 'post') {
  if($uri[1] !== 'posts'){
    header("HTTP/1.1 404 Not Found");
    exit();
  }
}

// endpoints starting with `/posts` for POST/PUT/DELETE results in a 404 Not Found
if ($uri[1] == 'posts' and isset($uri[2])) {
    header("HTTP/1.1 404 Not Found");
    exit();
}

// the post id is, of course, optional and must be a number
$postId = null;
if (isset($uri[2])) {
    $postId = (int) $uri[2];
}

$requestMethod = $_SERVER["REQUEST_METHOD"];

// pass the request method and post ID to the Post and process the HTTP request:
$controller = new Post($dbConnection, $requestMethod, $postId);
$controller->processRequest();
```

`src/Post.php`

```php
<?php
namespace Src;

class Post {
  private $db;
  private $requestMethod;
  private $postId;

  public function __construct($db, $requestMethod, $postId)
  {
    $this->db = $db;
    $this->requestMethod = $requestMethod;
    $this->postId = $postId;
  }

  public function processRequest()
  {
    switch ($this->requestMethod) {
      case 'GET':
        if ($this->postId) {
          $response = $this->getPost($this->postId);
        } else {
          $response = $this->getAllPosts();
        };
        break;
      case 'POST':
        $response = $this->createPost();
        break;
      case 'PUT':
        $response = $this->updatePost($this->postId);
        break;
      case 'DELETE':
        $response = $this->deletePost($this->postId);
        break;
      default:
        $response = $this->notFoundResponse();
        break;
    }
    header($response['status_code_header']);
    if ($response['body']) {
        echo $response['body'];
    }
  }

  private function getAllPosts()
  {
    $query = "
      SELECT
          id, title, body, author, author_picture, created_at
      FROM
          post;
    ";

    try {
      $statement = $this->db->query($query);
      $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }

    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body'] = json_encode($result);
    return $response;
  }

  private function getPost($id)
  {
    $result = $this->find($id);
    if (! $result) {
        return $this->notFoundResponse();
    }
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body'] = json_encode($result);
    return $response;
  }

  private function createPost()
  {
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (! $this->validatePost($input)) {
      return $this->unprocessableEntityResponse();
    }

    $query = "
      INSERT INTO post
          (title, body, author, author_picture)
      VALUES
          (:title, :body, :author, :author_picture);
    ";

    try {
      $statement = $this->db->prepare($query);
      $statement->execute(array(
        'title' => $input['title'],
        'body'  => $input['body'],
        'author' => $input['author'],
        'author_picture' => 'https://secure.gravatar.com/avatar/'.md5(strtolower($input['author'])).'.png?s=200',
      ));
      $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }

    $response['status_code_header'] = 'HTTP/1.1 201 Created';
    $response['body'] = json_encode(array('message' => 'Post Created'));
    return $response;
  }

  private function updatePost($id)
  {
    $result = $this->find($id);
    if (! $result) {
      return $this->notFoundResponse();
    }
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (! $this->validatePost($input)) {
      return $this->unprocessableEntityResponse();
    }

    $statement = "
      UPDATE post
      SET
        title = :title,
        body  = :body,
        author = :author,
        author_picture = :author_picture
      WHERE id = :id;
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array(
        'id' => (int) $id,
        'title' => $input['title'],
        'body'  => $input['body'],
        'author' => $input['author'],
        'author_picture' => 'https://secure.gravatar.com/avatar/'.md5($input['author']).'.png?s=200',
      ));
      $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body'] = json_encode(array('message' => 'Post Updated!'));
    return $response;
  }

  private function deletePost($id)
  {
    $result = $this->find($id);
    if (! $result) {
      return $this->notFoundResponse();
    }

    $query = "
      DELETE FROM post
      WHERE id = :id;
    ";

    try {
      $statement = $this->db->prepare($query);
      $statement->execute(array('id' => $id));
      $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body'] = json_encode(array('message' => 'Post Deleted!'));
    return $response;
  }

  public function find($id)
  {
    $query = "
      SELECT
          id, title, body, author, author_picture, created_at
      FROM
          post
      WHERE id = :id;
    ";

    try {
      $statement = $this->db->prepare($query);
      $statement->execute(array('id' => $id));
      $result = $statement->fetch(\PDO::FETCH_ASSOC);
      return $result;
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }

  private function validatePost($input)
  {
    if (! isset($input['title'])) {
      return false;
    }
    if (! isset($input['body'])) {
      return false;
    }

    return true;
  }

  private function unprocessableEntityResponse()
  {
    $response['status_code_header'] = 'HTTP/1.1 422 Unprocessable Entity';
    $response['body'] = json_encode([
      'error' => 'Invalid input'
    ]);
    return $response;
  }

  private function notFoundResponse()
  {
    $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
    $response['body'] = null;
    return $response;
  }
}
```

Let's start the PHP Server and test your APIs with a tool like [Postman](https://www.postman.com/downloads/).

```bash
php -S localhost:8000 -t api
```

# Done

Congratulations!! You have successfully built a REST API.

GitHub: https://github.com/shahbaz17/php-rest-api

# What's Next

Now, as you have built this application, you may be aware that these endpoints are not protected, and anyone on the internet could take advantage and update/delete/insert data. So, Let's secure this API with authentication and authorization.

In my next article, I will cover how you can take advantage of [Magic's](https://magic.link/) Passwordless feature and secure your API with just a few lines of changes to these codes.

{% post magiclabs/secure-your-php-rest-api-with-magic-82k %}