REST API implementation with dynamic routing

This is a simple REST API implementation written in PHP. It is designed to be very dynamic and easy to re-use.

The magic for the routing happens in the .htaccess file. It routes the incoming calls to the main file that handles the requests.

RewriteEngine On

# create a person (HTTP POST)
RewriteRule ^api/v1/persons/create/?$ api.php?class=person&method=create [QSA,L]

# update a person (HTTP PUT), <id>
RewriteRule ^api/v1/persons/([A-Za-z0-9-]+)/?$ api.php?class=person&method=update&id=$1 [QSA,L]


The Helper class contains some basic functions that are commonly used.

class Helpers {
    /**
     * Sanitize a given string
     * @param value
     * @return value
     */
    public static function sanitizeString($value){
        $value = filter_var(trim($value), FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
        return $value;
    }

    /**
     * Sanitize a given array
     * @param array
     * @return array
     */
    public static function sanitizeArray($array){
        if(is_array($array)){
            foreach($array as $arraykey=>$arrayvalue){
                if(is_array($arrayvalue)){
                    $array[$arraykey] = Helpers::sanitizeArray($arrayvalue);
                }
                else if(strlen($arrayvalue) > 0) {
                    $array[$arraykey] = Helpers::sanitizeString($arrayvalue);
                }
            }
        }
        return $array;
    }
}


The BaseClass represents a class that can be used for Models that need dynamic accessor methods [1].

class BaseClass {
    /**
     * Dynamic getters and setters
     * @param method, params
     */
    public function __call($method, $params){
        $pre = substr($method, 0, 3);
        $var = lcfirst(substr($method, 3));
        if($pre == 'get'){
            return $this->{$var};
        }
        else if($pre == 'set'){
            $this->{$var} = $params[0];
        }
        else if($pre == 'out'){
            return htmlspecialchars($this->$var, ENT_QUOTES, 'UTF-8'); //prevent XSS
        }
    }
}


This is the main API class which processes the incoming data and intercepts every call to the API. This can be very useful if you want to log these requests in just one place.

class MyAPI extends BaseClass {
    protected $data = [];

    /**
     * Saves the passed data into the object instance,
     * so that the subclasses can access this data
     */
    public function __construct(){
        $rawinput = file_get_contents('php://input');
        $this->data['input'] = Helpers::sanitizeArray(json_decode($rawinput,true));
    }

    /**
     * Magic method to intercept all the API calls
     * @param method<string>, args<array>
     */
    public function __call($method, $args){
        /**
         * Log all the API calls
         */
        if(strpos($method,'__log') !== false){
            $method = str_replace('__log','',$method);
            if(method_exists($this,$method)){
                $this->{$method}($args[0]); //call the implemented method
            }
        }
        else {
            /**
             * Dynamic accessor methods
             */
            return parent::__call($method, $args);
        }
    }
}


The endpoints and actions are defined in the class of the resource (eg. Person)

class Person extends MyAPI {
    /**
     * POST /v1/person/create
     * creates a new person
     *
     * params
     * - firstname <string>
     * - lastname <string>
     * - ...
     */
    public function create($args){
        $response = ['responseCode' => 'OK', 'id' => 123];
        http_response_code(201);
        header('Content-Type: application/json');
        echo json_encode($response);
    }

    /**
     * PUT /v1/person/<id>
     * updates the persons data
     *
     * params
     * - firstname <string>
     * - lastname <string>
     * - ...
     */
    public function update($args){
        $response = ['responseCode' => 'OK'];
        http_response_code(200);
        header('Content-Type: application/json');
        echo json_encode($response);
    }
}


This is the entry point of the API. It instantiates the requested class and calls the corresponding method. The interceptor logs the requests and forwards the request to the right object. For security reasons it checks if the method exists and won't allow other calls.

/**
 * Content-Type: application/json; charset=utf-8
 * Accept: application/json
 */
namespace MyProject\API;

/* require_once for all the required classes here */

//instantiate the requested classes and call the specified methods based on the routing
if(isset($_GET['class']) && in_array(strtolower($_GET['class']),['person'])){
    $class = '\MyProject\API\\'.ucfirst($_GET['class']);
    $reflectionClass = new \ReflectionClass($class);
    $classMethods = $reflectionClass->getMethods();
    $obj = new $class;
    $method = $_GET['method']??'';
    if($obj instanceof MyAPI && !in_array($method,['__call','__construct']) && method_exists($obj,$method) && in_array($method,array_column($classMethods,'name'))){
        //call the logging method before the actual method (inspired by aspect-oriented programming)
        $obj->{$method.'__log'}($_GET);
    }
}


To test the API you can simply call it using CURL with the given data

# create a person
curl -X POST \
https://{your-domain}/api/v1/persons/create \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-d '{"firstname":"Anakin","lastname":"Skywalker"}'  

# update a person
curl -X PUT \
https://{your-domain}/api/v1/persons/123 \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-d '{"firstname":"Darth","lastname":"Vader"}'


Sources:

[1] https://blog.cipher.digital/article/dynamic-getters-and-setters-in-php

Tags: php, rest, api


« back

010100100110000101101110011001000110111101101101010000110110100101110000011010000110010101110010

About the author

human, software engineer, tech enthusiast, security researcher

E-Mail: blog@cipher.digital

-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: OpenPGP (RSA-2048)

xsBNBFjAYL4BCACsmBS6zE+0b7mZVtQhmfnRn3+IIQfT6WlE6izM39Q42yxj
Hf2GOZU15Xc1x5RM9ZZx7HnMyTQWJMkwCzEba4Ju8dbn8gbFzLFp+mXAWQVJ
NOhsLvt58X/k1nQ3HYaYAbJPFE4k89zlFUjBG+a1Qs0kNg5RkaSTcE4iV6L4
749LYRba1VFK1p3eIFmIh1zQnzwFY1WYJjvXHURZel8MA0BJTkmfOW4MRHZL
lz8mjmTeWoRyxismRDprEtGynK7oIb3qUKAIr5MtoyESHBhVR+EpWHP0+06T
IfOsrsp8maNztXRQRKZxHzNZj/ayGpxBGO19e0/6jNpWGI5Nflwo/oHbABEB
AAHNI0RpZ2l0YWxDaXBoZXIgPGJsb2dAY2lwaGVyLmRpZ2l0YWw+wsByBBAB
CAAmBQJYwGDQBgsJCAcDAgkQMsB3T2XG/XYEFQgCCgMWAgECGwMCHgEAAOzo
B/4obbCU7u4f8kXQiaqAhSCjjyR5ZzdApPCh9i9XJ0qGTULTUuBrin1JDXSj
HoiByL2mYh92+I8S+YMWLMiTQzl9O4wx+A0eDnfwbs5jKJSQt5Pc8NMlwWKU
pG+R7escZ7le/qJYMgGPUWzFhgaKi8jueMW/NJSmPu/Tu4V9nhyxG9oaV3oP
rF+W0bekP84tDJ477clRSSK9ZzjMbLL1PWuNmCd8Gsnd3fyP1WcadIMDrnBB
sb+7AQ9eTywJ4Yzogh+cWjwy+TkkfEyCJ0X2n5WPURWc0YOFVqhcV4TYDR4v
CHSbh+r7OVKIjqdQKDJwAUCYeSkePbxJYmzRoaTd2+RgzsBNBFjAYL4BCADE
i8WrXxZWn42DlKDpnwTFBo/8asY4SJ22Zagkoj3cVvkechDWqQnWD753y5Xo
gymfPnNjoQGmClDaQoZ29kC4kHTmBPICHCCLvV/7YVCZC4WPpSnpklbllmk7
S8WTnyEm09gniGyLVy5st6MYmFDB4VnfXpzVYtpyEOyIfGV+JmuT90L872xc
+rI1/UuZA15k8M+ViD2xDlBMz3fbWxbt/KEUvbGoh2RW6SBJl1/z33ainQmO
oqygZtHhoFybqf/OUAHzASPcy+E4byWBIqwDDumKWfsd1YYkUgPMIxEvNaU3
2Olh5+2HX1y8WAf5cIfXUDfmZ88HmWVVXAK9JjztABEBAAHCwF8EGAEIABMF
AljAYNEJEDLAd09lxv12AhsMAACSqQf/Tz5KsfN3Yr82jXeO7jEWqI8yUaV2
vfK2JNfQXMIYDezIPxZU/sOOz9QF5gzHaLzt6moDQzHTZy9IE6q4l5gH1Wcm
1rX2b2b4ST3ThRzuDcfSCDZvUIAQ0WEBlXJZbCMwV8Rs5vsvv/CeXaT19zMb
CGD+23A1dKDSDmnlycCSDlTK0dc4flc8qqsMAXXtV7F370L3r76GQGj/ap57
k8K5l8VOqNCU2E8PJ1nU3Kf0fpaPJCpmDp51iZB6Ndx7ujb3qCzt5ND0Nqpz
8wuA9uuzf7LdYsz6MdDo3u8cBYeT2KA2pOA6W1SJgSx62Z4hFZxS5nseW3al
tfhqcXA+Ox3+gw==
=GS1N
-----END PGP PUBLIC KEY BLOCK-----