PHP Restful API Microservices

How difficult is to properly implement the microservices design pattern in PHP? It will definitely need some thinking and maybe to leave some convince behind. At the end of the day we should have an autonomous loosely coupled service with all the benefits (and drawbacks) of microservices.
In this article we do a little walk-through of a development process of a small RESTful microservice in PHP and we take a look at some Domain-Driven Design (DDD) theory as well.


Consider a simple CRUD service for a blog articles management. Via this REST API you can list, create, update and delete articles in the database (or whatever the persistence is).

We will implement this service following the microservices design pattern so we create a single REST endpoint (/articles) serving all the request. 
Similarly we would create endpoint for the categories and authors management.

First of all, let's prepare the infrastructure.

Database Schema and Test-Data

Traditionally, we use a MySQL database with a following schema: 

CREATE SCHEMA `blog` DEFAULT CHARACTER SET utf8;

CREATE TABLE `blog`.`categories` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(50) NOT NULL,
  PRIMARY KEY (`id`));
  
CREATE TABLE `blog`.`authors` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(50) NOT NULL,
  `email` VARCHAR(50) NOT NULL,
  PRIMARY KEY (`id`));

CREATE TABLE `blog`.`articles` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(100) NOT NULL,
  `summary` TEXT(1000) NOT NULL,
  `body` TEXT NOT NULL,
  `createdAt` DATE  NOT NULL,
  `categoryId` INT NOT NULL,
  `authorId` INT NOT NULL,
  PRIMARY KEY (`id`),
  INDEX `categoryId_idx` (`categoryId` ASC),
  INDEX `authorId_idx` (`authorId` ASC),
  CONSTRAINT `category_fk`
    FOREIGN KEY (`categoryId`)
    REFERENCES `blog`.`categories` (`id`),
  CONSTRAINT `author_fk`
    FOREIGN KEY (`authorId`)
    REFERENCES `blog`.`authors` (`id`)); 

Then, let's put one author, two categories and three articles into the database.

INSERT INTO `blog`.`authors` (`id`, `name`, `email`)
  VALUES (0, 'Tomas Tulka', 'tomas.tulka@gmail.com');

INSERT INTO `blog`.`categories` (`id`, `name`)
  VALUES (0, 'PHP'), (0, 'Java');

INSERT INTO `blog`.`articles` (`id`, `title`, `summary`, `body`, `createdAt`, `categoryId`, `authorId`)
  VALUES 
    (0, 'Sample PHP blog post', 
    'Lorem ipsum dolor sit amet.',
    'Sed vitae tincidunt magna. Sed pretium neque commodo mauris lobortis, quis finibus dolor malesuada.',
    '2018-05-01', 1, 1), 
    (0,  'Another PHP blog post', 
    'Donec id pellentesque elit, sit amet accumsan mi.',
    'Duis molestie tellus quis orci venenatis, ac pretium quam malesuada. Vivamus congue justo nulla, sit amet pharetra purus condimentum at.',
    '2018-05-02', 1, 1), 
    (0,  'Java blog post', 
    'Praesent porta sagittis diam non interdum.',
    'Semper a nunc nec dapibus. Sed tristique vel ipsum vitae euismod. Aenean vel nibh ac diam ullamcorper porta id at purus.',
    '2018-05-02', 2, 1);

After creating the database schema and putting some test data, we can discuss the service architecture design. 

REST API 

We will use the Richardson Maturity Model approach of HTTP verbs, simply: 

  • GET endpoint - listing items
  • GET endpoint/{id} - item detail
  • POST endpoint - creating a new item
  • PUT endpoint/{id} - updating an existing item
  • DELETE endpoint/{id} - deleting an existing item

Microservices and Domain-Driven Design

We will model our microservice around a DDD Aggregate, in this case around the Article.

For sake of simplicity we model our Article aggregate as an Anemic Domain Entity. With a maturer domain model the entity should be modeled as immutable object, created only via a constructor or a Factory, containing only getters and behavior methods. Such a domain object shouldn't be definitely exposed to the client's view (like we are doing), but should be transformed into a DTO.

domain/Article.php

class Article { 
    public $id;
    public $title;
    public $summary;
    public $body;
    public $createdAt;
    
    public $categoryId;
    
    public $author;         
}

class ArticleAuthor {  
    public $id;
    public $name;
    public $email;
} 

Why is there the class ArticleAuthor, why don't we create a separate class Author for that purpose? Well, we just don't need all the data of the author, an article needs only few of them. If, in the future, more attributes will be added to the Author entity, the Article structure should stay untouched. The object of the class ArticleAuthor is an Value Object and is accessible only thru the aggregate's root. So remains the Article consistent event when the Author entity changes its structure. Instead of ArticleAuthor we can use the name Author within different namespaces (articles and authors).

Persistence

According the DDD theory, each aggregate has a matching repository. The repository is the mechanism you should use to retrieve and persist aggregates. Obviously, we will persist the data into a database, but the persistence is a point of decision which could be (and should be) made at the latest possible point. And this is exactly what a repository makes possible.

domain/ArticleRepo.php

require_once __DIR__ . '/Article.php';

interface ArticleRepo { 
    public function fetchAll($categoryId, $authorId, $start, $limit);    
    public function fetchOne($id);    
    public function create(Article $article);    
    public function update($id, Article $article);    
    public function delete($id);
}

This interface decouples the client code (the service) from the persistence decision. It can be for example implemented as an in-memory storage in the early phases of the development.

We create a PDO-based implementation:

infrastructure/ArticleRepoPDO.php

require_once __DIR__ . '/../domain/Article.php';
require_once __DIR__ . '/../domain/ArticleRepo.php';

class ArticleRepoPDO implements ArticleRepo {
 
    private $conn;
    
    private $articles_table = 'articles';
    private $articles_categories_table = 'categories';
    private $authors_table = 'authors';
  
    public function __construct(PDO $conn){                                           
        $this->conn = $conn;
    }
    
    function fetchAll($categoryId = null, $authorId = null, $start = 0, $limit = 10) {   
        $q = "SELECT a.id, a.title, a.summary, a.createdAt, a.categoryId, a.authorId, au.name authorName, au.email authorEmail
                FROM {$this->articles_table} a
                    LEFT JOIN {$this->authors_table} au ON a.authorId = au.id
                WHERE 1=1 ";
                
        $params = array('start' => (int)$start, 'limit' => (int)$limit);

        if ($categoryId) {
            $q .= " AND a.categoryid = :categoryId";
            $params['categoryId'] = (int)$categoryId;
        }
        if ($authorId) {
            $q .= " AND a.authorId = :authorId";
            $params['authorId'] = (int)$authorId;
        }                    
                    
        $q .="  ORDER BY a.createdAt DESC, a.id DESC
                LIMIT :start,:limit";
        
        $stmt = $this->conn->prepare($q);
        
        foreach ($params as $param => $value) {
            $stmt->bindValue($param, $value, PDO::PARAM_INT);
        }
        
        $stmt->execute();
        
        $articles = array();  
               
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)){
          $article = new Article();
          
          $article->id = (int)$row['id'];
          $article->title = $row['title'];
          $article->summary = $row['summary'];
          $article->createdAt = $row['createdAt'];
          $article->categoryId = (int)$row['categoryId'];
          
          $article->author = new ArticleAuthor();
          $article->author->id = (int)$row['authorId'];
          $article->author->name = $row['authorName'];
          $article->author->email = $row['authorEmail'];
          
          array_push($articles, $article);
        }
             
        return $articles;
    }
    
    public function fetchOne($id) {
        $q = "SELECT a.id, a.title, a.summary, a.body, a.createdAt, a.categoryId, a.authorId, au.name authorName, au.email authorEmail
                FROM {$this->articles_table} a
                    LEFT JOIN {$this->authors_table} au ON a.authorId = au.id
                WHERE a.id = :id ";
                        
        $stmt = $this->conn->prepare($q);        
        $stmt->bindValue('id', (int)$id, PDO::PARAM_INT);        
        $stmt->execute();
        
        $article = null;  
               
        if ($row = $stmt->fetch(PDO::FETCH_ASSOC)){
          $article = new Article();
          
          $article->id = (int)$row['id'];
          $article->title = $row['title'];
          $article->summary = $row['summary'];
          $article->body = $row['body'];
          $article->createdAt = $row['createdAt'];
          $article->categoryId = (int)$row['categoryId'];
          
          $article->author = new ArticleAuthor();
          $article->author->id = (int)$row['authorId'];
          $article->author->name = $row['authorName'];
          $article->author->email = $row['authorEmail'];
        }
             
        return $article;
    }
    
    public function create($article) {
        $q = "INSERT INTO {$this->articles_table} (id, title, summary, body, createdAt, categoryId, authorId)
                VALUES (0, 
                  '{$article->title}', 
                  '{$article->summary}', 
                  '{$article->body}', 
                  '{$article->createdAt}', 
                  {$article->categoryId}, 
                  {$article->authorId}
                )";
                        
        $stmt = $this->conn->prepare($q);        
        $stmt->execute(); 
        
        $article->id = $this->conn->lastInsertId();
        
        return $article;   
    }
    
    public function update($id, $article) {
        $q = "UPDATE {$this->articles_table}
                SET title = '{$article->title}', 
                    summary = '{$article->summary}', 
                    body = '{$article->body}', 
                    createdAt = '{$article->createdAt}', 
                    categoryId = {$article->categoryId}, 
                    authorId = {$article->authorId}
                WHERE id = :id";
                        
        $stmt = $this->conn->prepare($q);        
        $stmt->bindValue('id', (int)$id, PDO::PARAM_INT);        
        $stmt->execute();
                
        $count = $stmt->rowCount();        
        return $count > 0;   
    }
    
    public function delete($id) {
        $article = $this->fetchOne($id);
        if ($article === null) {
            return false;
        }
        
        $q = "DELETE FROM {$this->articles_table} WHERE id = :id ";
        
        $stmt = $this->conn->prepare($q);                                  
        $stmt->bindValue('id', (int)$id, PDO::PARAM_INT);        
        $stmt->execute();
        
        $count = $stmt->rowCount();        
        return $count > 0;
    }
} 

Constructor of the repository class needs a PDO database connection. We deal with that with a help of the Factory pattern.

We create a Database interface which must be implemented by all the persistence providers, MySQL in our case:

infrastructure/db/Database.php

interface Database { 
    public function getConnection();  // returns a PDO connection object
} 

infrastructure/db/DatabaseMySql.php

require_once __DIR__ . '/Database.php';

class DatabaseMySql implements Database {    
    private $conn;
    
    function __construct($host, $db, $username, $password) {
        try {
            $this->conn = new PDO("mysql:host={$host};dbname={$db}", $username, $password);
            $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->conn->setAttribute(PDO::MYSQL_ATTR_FOUND_ROWS, true);
            $this->conn->exec("set names utf8");
            
        } catch(PDOException $e){
            throw new Exception('Connection error: ' . $e->getMessage(), 0, $e);
        }
    }
 
    public function getConnection() {
        return $this->conn;
    }
} 

Our factory has a static method for getting the right PDO connection:

infrastructure/db/DatabaseFactory.php

require_once __DIR__ . '/DatabaseMySql.php';

class DatabaseFactory {  
  public static function getDatabase($type, $host, $db, $username, $password) {
    switch ($type) {         
      case 'mysql':
        return new DatabaseMySql($host, $db, $username, $password);        
      default:
        throw new Exception('Unknown database type: ' . $type);
    } 
  }
}

The factory can be used as following:

$db = DatabaseFactory::getDatabase('mysql', 'localhost', 'blog', 'root', '1234');

Controller

The term comes from MVC design pattern. The responsibility of the controller in the application is to process the user input into output. This is the right place where the parameters should be validated (data types, range etc., the business validation is not meant here - that belongs to the domain services), proceeded and transformed back to the client.

The important point here is the controller shouldn't know anything about the fact, that we're developing a web application. The web-specifics must be present only in the application itself, as we show in a moment. 

application/ArticleController.php

require_once __DIR__ . '/../domain/ArticleRepo.php';

class ArticleController {

  private $repo;

  public function __construct(ArticleRepo $repo){                                           
    $this->repo = $repo;
  }

  public function detailRequest($id) {
    $article = $this->repo->fetchOne((int)$id);
    
    return $article;
  }
  
  public function listRequest($params) {
    $limit = 10;
    
    $categoryId = $this->getIfSet($params, 'categoryId');   
    $authorId = $this->getIfSet($params, 'authorId');   
    $page = $this->getIfSet($params, 'page', 0);
    
    $articles = $this->repo->fetchAll((int)$categoryId, (int)$authorId, $page * $limit, $limit);
    
    return $articles;
  }
  
  public function createRequest($params) {
    $article = new Article();
    $article->title = $params['title'];
    $article->summary = $params['summary'];
    $article->body = $params['body'];
    $article->createdAt = $params['createdAt'];
    $article->categoryId = (int)$params['categoryId'];
    $article->authorId = (int)$params['authorId'];
    
    if (!$article->title || !$article->summary || !$article->body || !$article->createdAt || !$article->categoryId || !$article->authorId) {
      return array('error' => 'Incorrect payload.');
    }
    
    $article = $this->repo->create($article);
    
    return (int)$article->id;
  }
  
  public function updateRequest($id, $params) {
    $article = new Article();
    $article->title = $params['title'];
    $article->summary = $params['summary'];
    $article->body = $params['body'];
    $article->createdAt = $params['createdAt'];
    $article->categoryId = (int)$params['categoryId'];
    $article->authorId = (int)$params['categoryId'];
    
    return $this->repo->update($id, $article);
  }
  
  public function deleteRequest($id) {  
    return $this->repo->delete($id);
  }
  
  // ///////// HELPER FUNCTIONS /////////////////////////////////////
  
  private function getIfSet($params, $var, $def = null) {
    return isset($params[$var]) ? $params[$var] : $def;
  }
}

Putting It All Together 

Blog Articles Architecture

The picture above shows the service components, double-lines represent layers boundaries. As you can see, all the inter-boundary communication is through interfaces, which define an API of every layer.

Finally we have all the blocks we need to build our service.

To keep things clean, we create a database configuration in a separate file:

config/db.config.php

define('DB_TYPE', 'mysql'); 
define('DB_HOST', 'localhost'); 
define('DB_NAME', 'blog'); 
define('DB_USER', 'root'); 
define('DB_PASS', '1234'); 

This configuration will be loaded withing the application code.

The application code is here to serve the request and assembly the components, this the only "dirty" code doing all the injections; a dependency-injection framework could be used here as well:

articles.php

header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
  
require_once __DIR__ . '/config/db.config.php';
require_once __DIR__ . '/application/ArticleController.php';
require_once __DIR__ . '/infrastructure/ArticleRepoPDO.php';
require_once __DIR__ . '/infrastructure/db/DatabaseFactory.php';

$db = DatabaseFactory::getDatabase(DB_TYPE, DB_HOST, DB_NAME, DB_USER, DB_PASS);

$repo = new ArticleRepoPDO($db->getConnection());

$controller = new ArticleController($repo);

$response = null;

switch ($_SERVER['REQUEST_METHOD']) {  
  case 'GET':
    if (isset($_GET['id'])) {
      $response = $controller->detailRequest($_GET['id']);
      
    } else {
      $response = $controller->listRequest($_GET);
    }
    if ($response === null) {
      http_response_code(404);
      
    } else {
      echo json_encode($response);
    }
    break;
    
  case 'POST':
    $_DATA = parseRequestData();
    $response = $controller->createRequest($_DATA);
    
    if ($response === null) {
      http_response_code(400);       
    
    } else {
      http_response_code(201);
      echo json_encode($response);
    }
    break;                           
  
  case 'PUT':
    if (!isset($_GET['id'])) {
      http_response_code(400);
      echo json_encode(array('error' => 'Missing "id" parameter.'));
    }
  
    $_DATA = parseRequestData();
    $response = $controller->updateRequest($_GET['id'], $_DATA);
    
    if ($response === false) {
      http_response_code(404);
      echo json_encode(array('error' => 'Article not found.'));
    } else {
      http_response_code(204);
    }
    break;
    
  case 'DELETE':
    if (!isset($_GET['id'])) {
      http_response_code(400);
      header("Location: {$_SERVER['REQUEST_URI']}/{$response}");
    }
    
    $response = $controller->deleteRequest($_GET['id']);
    
    if ($response === false) {
      http_response_code(404);
      echo json_encode(array('error' => 'Article not found.'));
    } else {
      http_response_code(204);
    } 
    break;
    
  case 'OPTIONS':
    header('Allow: GET POST PUT DELETE OPTIONS');
    break;
        
  default:
    http_response_code(405);
    header('Allow: GET POST PUT DELETE OPTIONS');
}

// ///////// HELPER FUNCTIONS /////////////////////////////////////

function parseRequestData() {
  $contentType = explode(';', $_SERVER['CONTENT_TYPE']);
  $rawBody = file_get_contents('php://input');
  $data = array();
  
  if (in_array('application/json', $contentType)) {
    $data = json_decode($rawBody, true);
    
  } else {
    parse_str($data, $data);
  }
  
  return $data;
}

Production

In production we want our API to be agnostic of the underlying technology. We can achieve that via setting rules of the HTTP server:

.htaccess

RewriteEngine On
RewriteRule ^articles$ articles.php [NC,L]
RewriteRule ^articles/([0-9]+)$ articles.php?id=$1 [NC,L] 

And now we can call the endpoint without the .php suffix.

Usage 

The usage is pretty straight-forward:

curl http://localhost/articles
curl http://localhost/articles?categoryId=1
curl http://localhost/articles?authorId=1
curl http://localhost/articles?categoryId=1&authorId=1

curl http://localhost/articles/1

curl http://localhost/articles -X POST -H "Content-Type: application/json" -d @data.json

curl http://localhost/articles/4 -X PUT -H "Content-Type: application/json" -d @data.json

curl http://localhost/articles/4 -X DELETE 

data.json

{ "title": "New article", 
  "summary": "Lorem ipsum", 
  "body": "Lorem ipsum dolor sit amet", 
  "createdAt": "2018-05-03", 
  "categoryId": 1, 
  "authorId": 1 
} 

Source Code 

You can find the whole project source code on GitHub.

Enjoy!