IoT at the Edge, building the Server API

Building a REST API using PHP

Why PHP for REST API?

I currently run this website on a pretty small 1 vCPU 512MB environment on AWS and it’s perfectly fine for a statically generated website such as this. In fact with AWS’s CDN it’s proven to be very fast. This is the real joy of static site generators such as Jekyll over classic database driven CMS environments such as WordPress, along with being substantially more secure. In fact as a static site is really all about network and storage I/O, the CPU utilisation has bearly risen above 0.2%. The idea of deploying my website to a VPC environment rather than shared hosting or S3, was that I’d have a Cloud ‘sandpit’ for other projects and experiments.

I thought I’d share my experience of building an REST API using PHP and provide a bit of a tutorial and hints and tips for anyone else out there wanting to do something similar.

The Technical Platform

The technical platform is NGINX on Amazon Linux AMI with PHP-FPM deployed on UNIX sockets. So given I didn’t want to go the expense of increasing my Cloud resources and costs, I decided I’d leverage the base environment to build my REST API server. I have been doing a lot of development in Python recently, for both Data Science and APIs. I’ve become a fan of OpenAPI 3.0 and have been using the Flask based Connexion OpenAPI framework.

Once I knew I was going to be building on PHP (7.2) I started to look around for a REST API framework. Swagger OpenAPI Generator supports PHP for the Slim framework, but I’m not a fan of Slim. Also the code generated for the Slim framework didn’t really provide that much acceleration or OpenAPI 3.0 validation. Nowhere near as much as the Python Connexion framework supports. However, deploying a Python Flask based runtime didn’t seem to make sense. I would just take another slice of memory. Also integrating HTTPS SSL with NGINX to PHP-FPM was going to be simpler than setting up a reverse proxy to a Flask server.

It’s been a while since I’ve done much development in PHP so I trawled the web for the current state of PHP MVC frameworks. In the end I opted for CodeIgniter 4.0. I’ve used CodeIgniter 3.0 in the past and have liked it’s balance of performance, ease of use and minimalism, yet is still feature rich.

MySQL / MariaDB is normally the database of choice with PHP, however MySQL is quite a memory hog. Although I have enough physical memory space to deploy MySQL I figured that it would take away cache being used by NGINX and so impact my website performance. So, I opted for the simpler route of SQLite3. Given the transaction volumes I am planning to put through this service, SQLite3 should be good enough, particularly as it’s deployed on a fast SSD.

A Quick Note on the PHP Debate

Historically PHP was seen by a lot of Software Engineers as a poor relation to languages such as C++, Java, Python and, more recently, NodeJS for backend web development.

PHP originally started out as an interpreted scripting language for Web Servers,. It suffered from weak typing and “spaghetti code” websites. However over the years it’s evolved and addressed a lot of the original deficiencies and criticisms. With the advent of PHP 7.x and the PHP-FPM daemon it’s also become a pretty performant platform. Even in 2020, with the rise of platforms such as NodeJS, Ruby and Go, ~80% of the Web is powered by PHP enabled sites - a lot of these are likely to be CMS platforms such as WordPress and Dupal. Even so, that’s a serious figure.

With strong OO support, strict typing and mature MVC frameworks such as CodeIgniter and Lavarel, there is no excuse for poor architecture, design and coding with PHP 7.x.

Lets get to the Code

The figure below outlines the basic architecture for the API. As mentioned, the database is SQLite3 accessed via the PHP PDO (PHP Data Objects) database driver framework. PDO provides a consistent API to all supported database, including non-relational such as MongoDB.

Models are CodeIgniter’s ORM framework. I did look at going full on Domain-Driven Design with “proper” Entity definitions and a Repository pattern, but I thought it was overkill for the application. In terms of DDD for PHP I recommend you take a look at the code examples from the book Domain-Driven Design in PHP.

Commands are a wrapper the the API interface, their main job is to perform validation on the parameters being passed to the API.

Controllers are self explantory and handle the HTTP routes and URLs.

Filters are a neat feature of CodeIgniter and allow you to insert processing logic to one or more routes and controllers pre and post response. This is where I’ve deployed API authentication.

Router is where URL routes are wired to the Controller.

Data Model and Database Design

This API is to support my IoT device. The data model is pretty straightforward. I simply need to store a series of Measurement Events, each with a Site which identifies the device sending the measurements, Timestamp (POSIX Time), Name of the measurement, the Data_Value itself and an optional Description.

The DDL for the database is shown below.

CREATE TABLE "measurement_event" 
( 
    `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 
    `site_name` TEXT NOT NULL, 
    `date_time` INTEGER NOT NULL, 
    `name` TEXT NOT NULL, 
    `data_value` NUMERIC, 
    `description` TEXT 
);

Configuring CodeIgniter for SQLite3

This is pretty straight forward for CodeIgniter, simply edit Database.php in your App Config directory and change the parameters as below. A number of the parameters are for MySQL only, e.g. Port, and ignored for a DBDriver of SQLite3.

    /**
	 * The default database connection.
	 *
	 * @var array
	 */
	public $default = [
		'DSN'      => '',
		'hostname' => 'localhost',
		'username' => '',
		'password' => '',
		'database' => APPPATH.'Database/iot.db',
		'DBDriver' => 'SQLite3',
		'DBPrefix' => '',
		'pConnect' => false,
		'DBDebug'  => true,
		'cacheOn'  => false,
		'cacheDir' => '',
		'charset'  => 'utf8',
		'DBCollat' => 'utf8_general_ci',
		'swapPre'  => '',
		'encrypt'  => false,
		'compress' => false,
		'strictOn' => false,
		'failover' => [],
		'port'     => 3306,
    ];

Defining the Model

Next we need to define the Model. The code snippet below highlights a function for either retrieving all measurements or a single measurement by Id.

 	/**
	 * Return all Measurements or single Measurement
     * 
     * @param integer $id
     * 
     * @return array
	 */
    public function findAllOrById($id = false)
    {
        if ($id == false) {
            return $this->findAll();
        } 
        else 
        {
            return $this->asArray()
                        ->where(['id' => $id])
                        ->first();
        }
    }

The following function uses CodeIgniters ability to run a custom SQL query.

 	/**
	 * Return Measurements filtered by:
     *  
     * @param string  $siteName
     * @param string  $name
     * @param integer $fromPOSIXTime
     * @param integer $toPOSIXTime
     * 
     * @return array
     * */
    public function findFiltered($siteName,$name,$fromPOSIXTime,$toPOSIXTime)
    {
        $sql = 'SELECT *
                FROM   measurement_event
                WHERE  date_time >= :fromDateTime:
                AND    date_time <= :toDateTime:
                AND    lower(site_name) like lower(:siteName:)
                AND    lower(name) like lower(:name:)';
        $query = $this->db->query($sql,[
            'fromDateTime' => $fromPOSIXTime,
            'toDateTime'   => $toPOSIXTime,
            'siteName'     => $siteName,
            'name'         => $name
        ]);
        if (!$query)
        {
            log_message('error',$this->db->getError());
            throw new Exception("Database Error");
        }
        return $query->getResultArray();
	}

Building Out the Controller

Next we need to build the Controller. CodeIgniter has a special sub class of Controller called a ResourceController specifically designed with helpers for REST APIs. The constructor instaniates a private instance of the Measurement model.

	/**
	 * Constructor.
	 */
    public function __construct()
    {
        // Create instance of Measurement Model.
        $this->model = new Measurement();
	}

CodeIgniter route mapping supports a standard function call set to index(), show() and create(). Index() maps to HTTP GET /measurements, show() to HTTP GET /measurement/{id}

 	/**
	 * Responds to GET /measurements to return all 
     * Measurement Events data.
     * 
     * @return CodeIgniter\HTTP\Response
	 */
    public function index()
    {
        // If no query parameters supplied, get all measurements.
        if (empty($this->request->uri->getQuery()))
        {
            return $this->getAll();
        }       
        // else run filtered query.
        else
        {
            return $this->getFiltered();
        }
    }

    /**
	 * Responds to GET /measurements/{id} to return a single 
     * Measurement Event by Id.
     * 
     * @return CodeIgniter\HTTP\Response
	 */
    public function show($id = null)
    {
        try
        {
            return $this->respond($this->model->findAllOrById($id));
        }
        catch (Exception $e)
        {
            return $this->failServerError();
        }

	}

The getFiltered function uses a Command class to validate the API query parameters and passes these to the model.

	/**
	 * Responds to GET /measurements/? to filtered 
     * Measurement Events data.
     * 
     * @return CodeIgniter\HTTP\Response
	 */
    private function getFiltered()
    {
        // Get URL parameters.
        try 
        {
            $params = $this->getQueryParameters();
            $filterMeasurements = new FilterMeasurements($params);
        }
        catch (ValidationException $e)
        {
            return $this->failValidationError();
        }
        try 
        {            
            $result = $this->model->findFiltered(
                $filterMeasurements->siteName,
                $filterMeasurements->name,
                $filterMeasurements->fromDatetime,
                $filterMeasurements->toDatetime
            );
            if (!empty($result))
            {
                return $this->respond($result);
            }
            else
            {
                return $this->failNotFound();
            }

        }
        catch (Exception $e)
        {
            return $this->failServerError();
        }
	}

Wiring Routes to the Controller

For a common consistent mapping CodeIgniter has a neat route wiring class against an API resource.

	/**
	* --------------------------------------------------------------------
	* Route Definitions
	* --------------------------------------------------------------------
	*/

	// We get a performance increase by specifying the default
	// route since we don't have to scan directories.
	$routes->resource('measurements');

This code maps a Controller class App\Measurement against the /measurements URI with a number of expected default function names index(), show() and create() etc.

Authentication

Lastly I used the Controller Filters function to apply authentication to the route.

 	public function before(RequestInterface $request)
    {
        $authenticated = false;
        try 
        {
            $apiValue = $_SERVER['HTTP_X_API_KEY'];
        }
        catch (Exception $e)
        {
            // Do nothing.
        }
        if (!is_null($apiValue))
        {   
           if (in_array($apiValue,$this->apiKeys)) 
           {
               $authenticated=true;
           }
        }        
        if (!$authenticated) 
        {
            $this->response->setBody('Unauthorized');
            $this->response->setStatusCode(401);
            return $this->response;
        }
	}   

Below shows how filters are wired to routes.

	// List filter aliases and any before/after uri patterns
	// that they should run on, like:
	//    'isLoggedIn' => ['before' => ['account/*', 'profiles/*']],
	// Protect measurements and all sub URLs.
	public $filters = [
		'apiauth' => ['before' => ['measurements','measurements/*']]
	];

NGINX URI Path Mapping

Lastly, in terms of deployment to an NGINX environment you need to configure NGINX with a series of URI rewrite rules to ensure these are understood by PHP-FPM.

 location /_iot {
       try_files $uri $uri/ /_iot/index.php$args;
       
               location ~ \.php$ {
           fastcgi_pass   unix:/opt/php/var/run/www.sock;
   fastcgi_index  index.php;
   fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
   include fastcgi_params;
  }

Summary

So that it’s, a very quick run through of building a REST API in CodeIgniter 4.0 PHP. The code is available in my Github repo. Hope you found it useful.