Tutorial - Restful API service in PHP


Posted on Apr 18th, 2016


REST, or in the full form, Representational State Transfer has become the standard design architecture for developing web APIs. In this sense an API - which stands for Application Programming Interface - allows for publicly exposed methods of an application to be accessed and manipulated outside of the program itself. A common usage of an API is when you wish to obtain data from a application  without having to actually visit the application itself. To allow this action to take place, the application has published an API that specifically allows for foreign applications to make calls to its data and return said data to the user from inside of the external application. On the web, this is often done through the use of RESTful URIs. 

The API that we're going to construct here will consist of two classes. One Abstract class that will handle the parsing of the URI and returning the response, and one concrete class that will consist of just the endpoints for our API. By separating things like this, we get a reusable Abstract class that can become the basis of any other RESTful API and have isolated all the unique code for the application itself into a single location.

Consider a url like this:

http://example.com/api/user/getUser/2

In MVC pattern, User is going to be the controller, getUser is going to be the method and 2 as a parameter. In this case our endpoint is user, verb is getUser and argument is 2. To make it simple for this example, we will not consider the controller. So our API URL will be like this:

http://example.com/api/getUser/2

The endpoint is getUser and argument is 2.

Nginx Configuration:

To make this url work, we have to change the server configuration of nginx. It will look something like this.

location /restapi {
       try_files $uri $uri/ /restapi/index.php?request=$request_uri;
}

Abstract Class:

Lets start creating the abstract class. We will allow this api to be accessed from any domain. So we need to appy appropriate headers.

<?php

header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json');

abstract class API
{
    /**
     * Property: method
     * The HTTP method this request was made in, either GET, POST, PUT or DELETE
     */
    protected $method = '';
    /**
     * Property: endpoint
     * The Model requested in the URI. eg: /files
     */
    protected $endpoint = '';
    /**
     * Property: verb
     * An optional additional descriptor about the endpoint, used for things that can
     * not be handled by the basic methods. eg: /files/process
     */
    protected $verb = '';
    /**
     * Property: args
     * Any additional URI components after the endpoint and verb have been removed, in our
     * case, an integer ID for the resource. eg: /<endpoint>/<verb>/<arg0>/<arg1>
     * or /<endpoint>/<arg0>
     */
    protected $args = Array();
    /**
     * Property: file
     * Stores the input of the PUT request
     */
    protected $file = Null;

    /**
     * Property: token
     * Stores the token
     */
    protected $token = Null;
    /**
     * Constructor: __construct
     * Allow for CORS, assemble and pre-process the data
     */
    public function __construct($request) {
       $this->_preFlightCheck();

        $this->args = explode('/', rtrim($request, '/'));
        $this->args = array_slice($this->args, 2);
        $this->endpoint = array_shift($this->args);
        if (array_key_exists(0, $this->args) && !is_numeric($this->args[0])) {
            $this->verb = array_shift($this->args);
        }

        $this->method = $_SERVER['REQUEST_METHOD'];
        if ($this->method == 'POST' && array_key_exists('HTTP_X_HTTP_METHOD', $_SERVER)) {
            if ($_SERVER['HTTP_X_HTTP_METHOD'] == 'DELETE') {
                $this->method = 'DELETE';
            } else if ($_SERVER['HTTP_X_HTTP_METHOD'] == 'PUT') {
                $this->method = 'PUT';
            } else {
                throw new Exception("Unexpected Header");
            }
        }

        switch($this->method) {
        case 'DELETE':
        case 'POST':
            $this->request = $this->_cleanInputs($_POST);
            break;
        case 'GET':
            $this->request = $this->_cleanInputs($_GET);
            break;
        case 'PUT':
            $this->request = $this->_cleanInputs($_GET);
            $this->file = file_get_contents("php://input");
            break;
        default:
            $this->_response('Invalid Method', 405);
            break;
        }
    }

    private function  _preFlightCheck()
    {
        // respond to preflights
        if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
            // return only the headers and not the content
            // only allow CORS if we're doing a GET - i.e. no saving for now.
            if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) && $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] == 'GET') {
                header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
                header('Access-Control-Allow-Headers: X-Requested-With, Authorization, Origin, Accept, Content-Type');
            }
            exit;
        }

    }

In the constructor we divide the url into parts. This gives us the method (GET, POST...etc)  being used, endpoint, verb and arguments. 

Understanding CORRS

_preFlightCheck is a private method which checks for preflight requests. 

With the preflighted requests a client can quickly know if the operation is allowed before sending a large amount of data, e.g., in JSON with PUT method. Or before travel sensitive data in authentication headers over the wire.

The fact of PUT, DELETE, and other methods, besides custom headers, aren't allowed by default(They need explicit permission with "Access-Control-Request-Methods" and "Access-Control-Request-Headers"), that sounds just like a double-check, because these operations could have more implications to the user data, instead GET requests. 

Authorization

We also need to validate this request by matching the credentials. The credentials can be an Authorization token or simply username/password. In javascript, we can create this token by using the btoa function:

btoa('username:password');

//outputs
//dXNlcm5hbWU6cGFzc3dvcmQ=

This token needs to be passed with every request.

The complete API abstract class will look something like this:

<?php

header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json');

abstract class API
{
    /**
     * Property: method
     * The HTTP method this request was made in, either GET, POST, PUT or DELETE
     */
    protected $method = '';
    /**
     * Property: endpoint
     * The Model requested in the URI. eg: /files
     */
    protected $endpoint = '';
    /**
     * Property: verb
     * An optional additional descriptor about the endpoint, used for things that can
     * not be handled by the basic methods. eg: /files/process
     */
    protected $verb = '';
    /**
     * Property: args
     * Any additional URI components after the endpoint and verb have been removed, in our
     * case, an integer ID for the resource. eg: /<endpoint>/<verb>/<arg0>/<arg1>
     * or /<endpoint>/<arg0>
     */
    protected $args = Array();
    /**
     * Property: file
     * Stores the input of the PUT request
     */
    protected $file = Null;

    /**
     * Property: token
     * Stores the token
     */
    protected $token = Null;
    /**
     * Constructor: __construct
     * Allow for CORS, assemble and pre-process the data
     */
    public function __construct($request) {
       $this->_preFlightCheck();

        $this->args = explode('/', rtrim($request, '/'));
        $this->args = array_slice($this->args, 2);
        $this->endpoint = array_shift($this->args);
        if (array_key_exists(0, $this->args) && !is_numeric($this->args[0])) {
            $this->verb = array_shift($this->args);
        }

        $this->method = $_SERVER['REQUEST_METHOD'];
        if ($this->method == 'POST' && array_key_exists('HTTP_X_HTTP_METHOD', $_SERVER)) {
            if ($_SERVER['HTTP_X_HTTP_METHOD'] == 'DELETE') {
                $this->method = 'DELETE';
            } else if ($_SERVER['HTTP_X_HTTP_METHOD'] == 'PUT') {
                $this->method = 'PUT';
            } else {
                throw new Exception("Unexpected Header");
            }
        }

        switch($this->method) {
        case 'DELETE':
        case 'POST':
            $this->request = $this->_cleanInputs($_POST);
            break;
        case 'GET':
            $this->request = $this->_cleanInputs($_GET);
            break;
        case 'PUT':
            $this->request = $this->_cleanInputs($_GET);
            $this->file = file_get_contents("php://input");
            break;
        default:
            $this->_response('Invalid Method', 405);
            break;
        }
    }

    private function  _preFlightCheck()
    {
        // respond to preflights
        if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
            // return only the headers and not the content
            // only allow CORS if we're doing a GET - i.e. no saving for now.
            if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) && $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] == 'GET') {
                header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
                header('Access-Control-Allow-Headers: X-Requested-With, Authorization, Origin, Accept, Content-Type');
            }
            exit;
        }

    }

    public function getToken()
    {
        /* Variables that will hold the credentials passed by user. */
        $token = null;

        // mod_php
        if (isset($_SERVER['PHP_AUTH_USER'])) {
            $username = $_SERVER['PHP_AUTH_USER'];
            $password = $_SERVER['PHP_AUTH_PW'];
            $token = base64_encode("$username:$password");
        // most other servers
        } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) {

                if (strpos(strtolower($_SERVER['HTTP_AUTHORIZATION']),'basic')===0)
                  $token = base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6));

        }
        if (is_null($token)) {
            header('HTTP/1.0 401 Unauthorized');
            return false;

        }
        return $token;

    }

    public function processAPI() {
        if (method_exists($this, $this->endpoint)) {
            return $this->_response($this->{$this->endpoint}($this->args));
        }
        return $this->_response("No Endpoint: $this->endpoint", 404);
    }

    private function _response($data, $status = 200) {
        header("HTTP/1.1 " . $status . " " . $this->_requestStatus($status));
        return json_encode($data);
    }

    private function _cleanInputs($data) {
        $clean_input = Array();
        if (is_array($data)) {
            foreach ($data as $k => $v) {
                $clean_input[$k] = $this->_cleanInputs($v);
            }
        } else {
            $clean_input = trim(strip_tags($data));
        }
        return $clean_input;
    }

    private function _requestStatus($code) {
        $status = array(
            200 => 'OK',
            404 => 'Not Found',
            405 => 'Method Not Allowed',
            500 => 'Internal Server Error',
        );
        return ($status[$code])?$status[$code]:$status[500];
    }
}

?>

 

Now to use this API, we are going to create another class which will extend the API class. In this class, we validate the token. We are not using database for this example, but in a real scenario, you should validate this token from the database.

Extend the Abstract Class

<?php

require_once 'classes/API.php';

class MyAPI extends API
{
    public function __construct($request, $origin) 
    {
        parent::__construct($request);
    }

    

    // Validate this token from database
    public function authorizeToken($token)
    {
        //if token exist, send true else false
        return ($token === 'dXNlcjpwYXNzd29yZA==') ? true: false;
    }

    /**
     * Example of an Endpoint
     */
     protected function getUser($id) {
        if ($this->method == 'GET') {
            $users = array(
                array('fname' => 'John', 'lname' => 'Smith'),
                array('fname' => 'James', 'lname' => 'Bond'),
                array('fname' => 'Donald', 'lname' => 'Duck'),
                array('fname' => 'Dumb', 'lname' => 'Fuck'),
            );
            if(isset($users[$id])) {
                return json_encode($users[$id]);
            }
            //if nothing found, send empty
            return json_encode(array());
        } else {
            return "Only accepts GET requests";
        }
     }
 }


/*******************************************
 * Handling the requests
 *******************************************/

 // Requests from the same server don't have a HTTP_ORIGIN header
if (!array_key_exists('HTTP_ORIGIN', $_SERVER)) {
    $_SERVER['HTTP_ORIGIN'] = $_SERVER['SERVER_NAME'];
}

try {

    $API = new MyAPI($_REQUEST['request'],$_SERVER['HTTP_ORIGIN']);
    $token = $API->getToken();

    if($API->authorizeToken($token)) 
    {
        echo $API->processAPI();
    }
    else
    {
        header('HTTP/1.0 401 Unauthorized');
        throw new Exception('Unauthorized');
    }

} catch (Exception $e) {
    echo json_encode(Array('error' => $e->getMessage()));
}

?>

Using the API (DEMO)

jQuery.ajax({
        type:'get',
        dataType: 'json',
        url:'http://playground.ajaxtown.com/restapi/getUser/1',
        beforeSend : function(req) {
            req.setRequestHeader('Authorization', "Basic dXNlcjpwYXNzd29yZA==");
        },
        success: function(data) {
          console.log(JSON.parse(data));
        }
});

This API is only intended for learning purpose. You should use bettern token mechanism for validations. Also make sure the token has an expiry date. There are concepts like short lived token and long lived token. Unfortunately explaining the whole mechanism is not viable in this one post. Hope this helps ! Drop your opinion with a comment.


The best of Javascript ECMAScript 6 or ES6Google API Key and Sender ID for Web Push Notifications

Comments


  • Apr 24, 2016, 10:09 AMCorey
    This is great! I can even use TCP to call the api and it doesn't breakdown, it handles everything (y)
  • May 2, 2016, 3:13 AMNitin
    Excellent. Very nicely explained. Thanks.
100% Complete