Web API written in PHP


WebAPI RESTfull PHP OOP Classes HTTP

Data is a valuable resource. With each passing day companies setup their web API in order to access their data (freely or via remuneration), but usually their access methods don't look standard or even easy to access from a logic point of view.

Today I'll show you how to create a flexible, meaningful, RESTful with multiple outputs API in PHP (and Apache).

The project is not that simple, but I'll try to organise the article in a logic flow.


Attention: this work is under the Creative Commons Attribution 3.0 Unported licence


web_api.zip


Files organisation

- .htaccess                // to use permalinks like /a/b/c instead of file.php?q=...
- api.php                  // the API controller, called when the API is reached by a user
- api/api.php              // the api class which will be extended in order to create the services
- api/yourservice.php      // your first service
- api/html/yourservice.php // your HTML output file for your service (optional)

File: .htaccess

It is the local Apache configuration file which will redirect the API calls to api.php. You can replicate this configuration for nginx, lighttpd or whatever server you're using.

For Apache, you need to enable mod_rewrite from your httpd.conf file.

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /uri/to/the/api

    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ api.php?q=$1 [L,QSA]
</IfModule>

Note: you have to edit the RewriteBase line or comment it depending on where you want to serve your API (i.e. http://yoursite.com/api/ you need to set it to /api; http://api.yoursite.com you need to comment it out).

File: api.php

As mentioned before, this is the API hardpoint from where all the requests start. I'll guide you reading the file in little chunks.

:::php
$path = explode("/", $_SERVER['PHP_SELF']);
$path = implode("/", array_slice($path, 0, -1));

define('BASE_URL', 'http://'.$_SERVER['HTTP_HOST'].'/'.$path);

This will help us knowing where the API actually is in spite of the fake URI used by the system.

:::php
// load the API classes
foreach(glob("api/*.php") as $file)
    require_once($file);

This loop will fetch all the services you've written and load them all in the script. In this way you don't need to edit api.php every time you add a new service.

:::php
$uri = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : null;

if(!$uri) {
    api::setStatus(400, "Bad request");
    exit(0);
}

// strip the path to the API from the URI
$uri = str_replace($path, '', $uri);

$request = explode('/', $uri);
array_splice($request, 0, 1); // strip the starting ''

The system is URI-based. Depending on the URI services are being called. api::setStatus will simply set the status code and message header. It is useful and should not be underestimated, as you should use that to signal errors (instead of outputting them in the HTTP payload as most of the developers do).

$request is the resulting API request (the URI part at least).

:::php
if(!class_exists($request[0])) {
    api::setStatus(501, "Not implemented");
    exit(0);
}

$request_data = $request[0]::formatRequest($request);
$api = new $request[0]($request_data);

This may appear a little bit confusing, but it's pretty simple instead. The API system expects that the first part of the URI is the class to be called, the rest are the (URI) parameters.

So we first check whenever the service (class) exists and if not it returns 501 and terminates the script. Otherwise we initialise the request data in $request_data (which may be used later in the HTML output too) using the class-specific request formatter, and then initialise the API service with the data we've previously formatted.

:::php
// analyse() must be overwritten in the extensions of class api
$result = $api->analyse();

analyse() is one of the methods we need to overwrite and is the heart of each service. Please notice that we're not outputting the result already, but saving it in a variable. This is because our API allows the user to choose between different outputs: JSON, XML or (fallback or intended) HTML.

This is the end of the first part of api.php. If you're tired, drink coffee and continue reading!


File: api.php (output results)

Welcome to the second part of api.php!

In this part we'll analyse the Accept HTTP header the user is sending (via its software or browser), taking the most wanted format and outputting the result in that form, if it's possible. If none of the requested formats are available, we print the result in text/html as fallback. More about HTTP's Accept field.

:::php
// get the accept formats from the HTTP headers
$accepts = explode(",", $_SERVER['HTTP_ACCEPT']);

// extract the accept formats
$accept = array();
foreach($accepts as $format) {
    $exp = explode(";", $format);

    if(!isset($exp[1]) || strpos($exp[1], "q=") === false)
        $accept['1'][] = $exp[0];
    else {
        $index = strpos($exp[1], "q=");
        $accept[(string)floatval(substr($exp[1], $index+2))][] = $exp[0];
    }
}

// from most to least desired type
arsort($accept);

First of all, we load the Accept header string from the request. Then we extract it into an array.


The various formats (in form of MIME types) are separated by a comma, options are divided by semicolons. Actually the only option is the optional "q" and represents the acceptance rating (default: 1): from 0 to 1, float (1 decimal). The higher, the better. This is for example the Accept header sent by my actual browser: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8. The last */* means "all the other types".


We're saving the accept types in the $accept['acceptance']['mime type'] format. We're using a string as key as floats are being converted into integers. An alternative would be to multiply all the acceptance rating by 10 and use integer keys. Eventually we sort the array from 1 (most desired) to 0 (least desired).

:::php
foreach($accept as $w => $array) {
        foreach($array as $type) {
            switch($type) {
                case "text/html":
                    if(!file_exists("api/html/".$request[0].".php"))
                        break;

                    header("Content-type: " . $type, true);
                    require_once("api/html/".$request[0].".php");

                    return;
                case "application/json":
                    if(!method_exists($api, "toJSON"))
                        break;

                    header("Content-type: " . $type, true);
                    echo $api->toJSON($result);

                    return;
                case "application/xml":
                    if(!method_exists($api, "toXML"))
                        break;

                    header("Content-type: " . $type, true);
                    echo $api->toXML($result);

                    return;
            }
        }
}

// fallback text/html
if(!file_exists("api/html/".$request[0].".php")) {
    api::setStatus(501, "Not implemented");
    exit(0);
}

header("Content-type: text/html");
require_once("api/html/".$request[0].".php");

This is pretty fast-forward. Cycle every accept type, in order of acceptance, and check if it is possible to output it. If it is, output it, otherwise continue untill you end the list: in this case fallback to HTML output or nothing, if it is not available. As we'll see, toXML() and toJSON() are not part of the base api class: in this way we've got more control on however the data is being outputted.


File: api/api.php

This is the barebone class of all of our services. Other than a base constructor, there are some utils.


In order to create a RESTful API, we don't need to use different URIs for accessing or manipulating the same resource. Of course we need to create different methods. Please refere to my previous article Meaningful API for more information.


:::php
public static function setStatus($status, $message) {
    header("HTTP/1.0 $status $message");
}

public static function formatRequest($request) {
    return null;
}

The first self explains, the second is just a placeholder which has to be overwritten.

:::php
public static function getPayload() {
    if(count($_POST) > 0)
        return $_POST;

    return file_get_contents('php://input');
}

This method retrieves the data via POST, PUT or custom methods. In case of POST, it will return an array (you can check it via is_array), otherwise it will return a string which needs to be parsed.

:::php
public function __construct($data) {
    if(!is_array($data))
        return;
    foreach($data as $var => $val) {
        if(property_exists(get_class($this), $var))
            $this->$var = $val;
    }
}

Pretty clever constructor: this will parse the $data variable (which is an array) and set the class' properties accordingly. In this way you don't need to do it manually in the extending classes. $data is the $request_data seen back in api.php, and is a formatted array from the class' formatRequest() method.

:::php
public function analyse() {
    return null;
}

Master class method which needs to be overwritten in the extending classes. It will process the request and return the result.


api/sum.php (example)

This is a "hello world"-like class which aims to let you understand how a service class should be written.

:::php
// It needs to be public or protected
// in order to be accessed by the
// parent's constructor.
protected $addends;

This is the declaration of class' properties. As commented, the properties have to be public or protected. If they're private, the api's constructor won't be able to set their values.

:::php
public static function formatRequest($request) {
    $result = array();

    // $request[0] is "sum", the name of the service
    for($i = 1; $i < count($request); $i++) {
        $result["addends"][] = floatval($request[$i]);
    }

    return $result;
}

This is the overwritten formatRequest() method. As you can see, it formats the "addends" key as we want the $addends variable to be.

:::php
public function __construct($data) {
    parent::__construct($data);

    // in this case we need to do nothing else
}

Nothing to do in this case: just call the parent's constructor.

:::php
public function analyse() {
    // in advanced cases, we should check for $_SERVER['REQUEST_METHOD']
    // in order to decide which method to fire. In our simple case, we'll
    // just call sum().

    return $this->sum();
}

As commented, here is where the method to be executed is chosen. In our simple case we only need to call the sum() method, but we could decide to create, update or delete a resource. We could recognise the URI is malformed or the data provided is not sufficient or correct, returning HTTP errors and exiting the script. This is the "controller" of the class and should be thought very carefully in order to avoid exploitation.

:::php
private function sum() {
    return array_sum($this->addends);
}

Oh well...

:::php
public static function toJSON($data, $pretty_print = false) {
    $return = array("result", $data);
    if($pretty_print && version_compare(phpversion(), "5.4.0") >= 0)
        return json_encode($return, JSON_PRETTY_PRINT);

    return json_encode($return);
}

Simple toJSON() method which converts the result of analyse() into a JSON object or array string (array, in this case). The $pretty_print argument lets the system choose whenever you have to include white spaces in a "pretty print" way. If the server runs an old PHP version, it will fallback to non-pretty printed version in any case. The pretty print is usefull when you have to output a human-readable object (like in the HTML format).

Note that I haven't included the toXML() method, but the process is the same.


File: api/html/sum.php

This is an example of HTML output file.

:::php
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Sum service</title>
    </head>
    <body>
        <h1>Sum operation</h1>
        <?php
            // $request_data and $result are accessible because
            // this is being executed in api.php
            echo implode(" + ", $request_data['addends']) . " = " . $result;
        ?>
        <h1>JSON output (Accept: application/json[;q=1]):</h1>
        <pre>
            <code>
<?php
echo sum::toJSON($result, true);
?>
            </code>
        </pre>
    </body>
</html>

The only thing to notice is that you can access all of the api.php variables, because this script is being executed at the end of it. The name of the file must match the name of the class.

- 15th July 2013

> back