In work and outside of work I develop a lot of RESTful services. Primarily I use ZendFramework for PHP REST Services due to the overwhelming complexity of the services, but I also create some very simple and straightforward services to provide very simple functionality while maintaining a super clean code base.
One such service is my mobile device authentication system. It makes use of almost all the HTTP Request Methods you could possibly want for a PHP REST Service, including:
GET
POST
PUT
DELETE
I've decided to publish this on my blog first and foremost so that you can see how I've gone about things. My idea with this PHP REST framework was to keep everything I needed in the top level file (in this case the index.php file).
Getting it setup is easy. First, get a folder structure together like this:
/library
        /library/Rest
/public
Now, for those of you who can't control your VirtualHost, in the /public folder, make a new file called '.htaccess'
In this file put in the following:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]
This file now tells your server to route all incoming requests (that don't match an existing path) to the index.php file.
Next, let's create the file called index.php in the /public folder. Leave this file empty for now.
In the /library/Rest folder create 3 files.
/library/Rest/Runner.php
/library/Rest/Request.php
/library/Rest/Response.php
The 'Request.php' and 'Response.php' are freebies if you will. You don't have to use these files, but they are generally my standard included files for any PHP Applications I create that don't run on ZendFramework.
So, I won't explain the code behind the Request or Response files, I like to keep my code commented, so you can see it for yourself. Essentially, the Request file controls the incoming HTTP Request, and the Response handles the outgoing HTTP Response.
As you already know, REST Services rely and operate on the HTTP Request and HTTP Response.
Here's the code for 'Request.php'
<?php
/**
 * Handles the incoming HTTP Request object.
 *
 * @package Rest_Runner
 * @copyright 2012 Roger E Thomas (http://www.rogerethomas.com)
 * @author Roger Thomas
 *
 */
class Rest_Request {
    /**
     *
     * @var Rest_Request
     */
    protected static $_request = null;
    /**
     * Holds the raw $_SERVER array
     *
     * @var array
     */
    private $serverArray = array ();
    /**
     * Raw array of all params, including $_GET, $_POST, and user params
     *
     * @var array
     */
    private $params = array ();
    /**
     * The raw $_GET array
     *
     * @var array
     */
    private $get = array ();
    /**
     * The raw $_POST array
     *
     * @var array
     */
    private $post = array ();
    /**
     * Body of response, or false if none manuall set
     *
     * @var mixed:boolean string
     */
    private $body = false;
    /**
     * Holds any manually set parameters when using:
     * $this->setParam()
     *
     * @var array
     */
    private $userParams = array ();
    /**
     * Initiate the class
     */
    public function __construct() {
        $this->_buildParams ();
        $this->serverArray = $_SERVER;
    }
    /**
     * Return instance of Rest_Request
     *
     * @return Rest_Request
     */
    public static function getRequest() {
        ob_start ();
        if (null === self::$_request) {
            self::$_request = new self ();
        }
        return self::$_request;
    }
    /**
     * Retrieve the REQUEST_URI without and GET parameters
     *
     * @example /contact-us
     * @return string
     */
    public function getRequestUri() {
        if (isset ( $this->serverArray ['REQUEST_URI'] )) {
            $uri = $this->serverArray ['REQUEST_URI'];
        } else {
            $uri = "/";
        }
        if (strstr ( $uri, "?" )) {
            $uri = strstr ( $uri, "?", true );
        }
        return $uri;
    }
    /**
     * Is request via HTTPS
     *
     * @return boolean
     */
    public function isHttpsRequest() {
        if (empty ( $this->serverArray ['HTTPS'] ) || $this->serverArray ['HTTPS'] == "off") {
            return false;
        }
        return true;
    }
    /**
     * Retrieve the REQUEST_URI WITH and GET parameters
     *
     * @example /contact-us
     * @return string
     */
    public function getRawRequestUri() {
        if (isset ( $this->serverArray ['REQUEST_URI'] )) {
            $uri = $this->serverArray ['REQUEST_URI'];
        } else {
            $uri = "/";
        }
        return $uri;
    }
    /**
     * Retrieve a header from the request header stack and
     * optionally set a default value to use if key isn't
     * found.
     *
     * @param string $name
     * @param mixed:multitype $default
     * @return string
     */
    public function getHeader($name, $default = null) {
        if (empty ( $name )) {
            return $default;
        }
        $temp = 'HTTP_' . strtoupper ( str_replace ( '-', '_', $name ) );
        if (isset ( $this->serverArray [$temp] )) {
            return $this->serverArray [$temp];
        }
        if (function_exists ( 'apache_request_headers' )) {
            $method = 'apache_request_headers';
            $headers = $method ();
            if (isset ( $headers [$name] )) {
                return $headers [$name];
            }
            $header = strtolower ( $header );
            foreach ( $headers as $key => $value ) {
                if (strtolower ( $key ) == $name) {
                    return $value;
                }
            }
        }
        return $default;
    }
    /**
     * Return the REQUEST_METHOD from the SERVER global array
     *
     * @return string
     */
    public function getRequestMethod() {
        if (isset ( $this->serverArray ['REQUEST_METHOD'] )) {
            $m = $this->serverArray ['REQUEST_METHOD'];
        } else {
            $m = "GET";
        }
        return $m;
    }
    /**
     * Add a single parameter to the params stack
     *
     * @param string $name
     * @param mixed $value
     */
    public function setParam($name, $value) {
        $this->userParams [$name] = $value;
        $this->_buildParams ();
    }
    /**
     * Retrieve all request params (GET / POST and Manuall Set Params) as a
     * single array
     *
     * @return array
     */
    public function getParams() {
        return $this->params;
    }
    /**
     * Retrieve all POST params as an array
     *
     * @return array
     */
    public function getPostParams() {
        return $this->post;
    }
    /**
     * Retrieve all GET params as an array
     *
     * @return array
     */
    public function getGetParams() {
        return $this->get;
    }
    /**
     * Retrieve all request params (GET and POST) as a single array
     *
     * @return array
     */
    public function getParam($name, $default = null) {
        if (array_key_exists ( $name, $this->params )) {
            return $this->params [$name];
        }
        return $default;
    }
    /**
     * Check if the request is HTTP_GET
     *
     * @return boolean
     */
    public function isGet() {
        if (strtolower ( $this->getRequestMethod () ) == "get") {
            return true;
        }
        return false;
    }
    /**
     * Check if the request is HTTP_POST
     *
     * @return boolean
     */
    public function isPost() {
        if (strtolower ( $this->getRequestMethod () ) == "post") {
            return true;
        }
        return false;
    }
    /**
     * Check if the request is HTTP_PUT
     *
     * @return boolean
     */
    public function isPut() {
        if (strtolower ( $this->getRequestMethod () ) == "put") {
            return true;
        }
        return false;
    }
    /**
     * Check if the request is HTTP_DELETE
     *
     * @return boolean
     */
    public function isDelete() {
        if (strtolower ( $this->getRequestMethod () ) == "delete") {
            return true;
        }
        return false;
    }
    /**
     * Get the raw body, if any.
     * Else this will return false
     */
    public function getBody() {
        if ($this->body == false) {
            @$body = file_get_contents ( 'php://input' );
            if (strlen ( trim ( $body ) ) > 0) {
                $this->body = $body;
            } else {
                $this->body = false;
            }
        }
        if ($this->body == false) {
            return false;
        }
        return $body;
    }
    /**
     * Alias for self::getBody
     *
     * @see Rest_Request::getBody
     * @return string
     */
    public function getRawBody() {
        return $this->getBody ();
    }
    /**
     * Alias for self::getIp
     *
     * @see Rest_Request::getIp
     * @return string
     */
    public function getClientIp() {
        return $this->getIp ();
    }
    /**
     * Return the users IP Address
     *
     * @return string
     */
    public function getIp() {
        if (isset ( $this->serverArray ['HTTP_X_FORWARDED_FOR'] )) {
            return $this->serverArray ['HTTP_X_FORWARDED_FOR'];
        } else if (isset ( $this->serverArray ['HTTP_CLIENT_IP'] )) {
            return $this->serverArray ['HTTP_CLIENT_IP'];
        }
        return $this->serverArray ['REMOTE_ADDR'];
    }
    protected function _buildParams() {
        if (empty ( $this->get )) {
            foreach ( $_GET as $k => $v ) {
                $this->get [$k] = $v;
                $this->params [$k] = $v;
            }
        }
        if (empty ( $this->post )) {
            foreach ( $_POST as $k => $v ) {
                $this->post [$k] = $v;
                $this->params [$k] = $v;
            }
        }
        foreach ( $this->userParams as $k => $v ) {
            $this->params [$k] = $v;
        }
    }
    /**
     * Is the request an Ajax XMLHttpRequest?
     *
     * @return boolean
     */
    public function isXmlHttpRequest() {
        if ($this->serverArray ['X_REQUESTED_WITH'] == 'XMLHttpRequest') {
            return true;
        }
        return false;
    }
}
Moving swiftly on, here's the code for 'Response.php'
<?php
/**
 * Handles the outgoing HTTP Response object.
 *
 * @package Rest_Runner
 * @copyright 2012 Roger E Thomas (http://www.rogerethomas.com)
 * @author Roger Thomas
 *
 */
class Rest_Response {
    /**
     *
     * @var string
     */
    const DEFAULT_CONTENT_TYPE = 'text/html';
    /**
     *
     * @var Rest_Response
     */
    protected static $_response = null;
    /**
     *
     * @var string
     */
    protected $_rawBody = null;
    /**
     *
     * @var string
     */
    protected $_contentType = null;
    /**
     *
     * @var string
     */
    protected $_body = null;
    /**
     * Singleton instance which means this is redundant
     */
    protected function __construct() {
    }
    /**
     * Return instance of Rest_Response
     *
     * @return Rest_Response
     */
    public static function getResponse() {
        ob_start ();
        if (null === self::$_response) {
            self::$_response = new self ();
        }
        return self::$_response;
    }
    /**
     * Assign a HTTP Response code.
     *
     * @param integer $code
     * @return Rest_Response
     */
    public function setHttpCode($code) {
        $http_codes = $this->_getHttpResponseCodes ();
        if (! array_key_exists ( $code, $http_codes )) {
            $string = 'HTTP/1.0 500 ' . $http_codes [500];
        } else {
            $string = 'HTTP/1.0 ' . $code . ' ' . $http_codes [$code];
        }
        header ( $string );
        return $this;
    }
    /**
     * Associated array of HTTP Response codes
     *
     * @return array
     */
    protected function _getHttpResponseCodes() {
        return $http_codes = array (
                100 => 'Continue',
                101 => 'Switching Protocols',
                102 => 'Processing',
                200 => 'OK',
                201 => 'Created',
                202 => 'Accepted',
                203 => 'Non-Authoritative Information',
                204 => 'No Content',
                205 => 'Reset Content',
                206 => 'Partial Content',
                207 => 'Multi-Status',
                300 => 'Multiple Choices',
                301 => 'Moved Permanently',
                302 => 'Found',
                303 => 'See Other',
                304 => 'Not Modified',
                305 => 'Use Proxy',
                306 => 'Switch Proxy',
                307 => 'Temporary Redirect',
                400 => 'Bad Request',
                401 => 'Unauthorized',
                402 => 'Payment Required',
                403 => 'Forbidden',
                404 => 'Not Found',
                405 => 'Method Not Allowed',
                406 => 'Not Acceptable',
                407 => 'Proxy Authentication Required',
                408 => 'Request Timeout',
                409 => 'Conflict',
                410 => 'Gone',
                411 => 'Length Required',
                412 => 'Precondition Failed',
                413 => 'Request Entity Too Large',
                414 => 'Request-URI Too Long',
                415 => 'Unsupported Media Type',
                416 => 'Requested Range Not Satisfiable',
                417 => 'Expectation Failed',
                418 => 'I\'m a teapot',
                422 => 'Unprocessable Entity',
                423 => 'Locked',
                424 => 'Failed Dependency',
                425 => 'Unordered Collection',
                426 => 'Upgrade Required',
                449 => 'Retry With',
                450 => 'Blocked by Windows Parental Controls',
                500 => 'Internal Server Error',
                501 => 'Not Implemented',
                502 => 'Bad Gateway',
                503 => 'Service Unavailable',
                504 => 'Gateway Timeout',
                505 => 'HTTP Version Not Supported',
                506 => 'Variant Also Negotiates',
                507 => 'Insufficient Storage',
                509 => 'Bandwidth Limit Exceeded',
                510 => 'Not Extended'
        );
    }
    /**
     * Return a header from the stack, or empty string
     * if not set.
     *
     * @param string $name
     * @return string
     */
    public function getHeader($name) {
        $headers = $this->getHeaders ();
        if (! array_key_exists ( $name, $headers )) {
            return "";
        }
        return $headers [$name];
    }
    /**
     * Assign a response header to be sent when issuing
     * the http response
     *
     * @param string $name
     * @param string $value
     * @param boolean $overrideExisting
     * @return Rest_Response
     */
    public function setHeader($name, $value, $overrideExisting = true) {
        if (strtolower ( $name ) == "content-type") {
            $this->_contentType = $value;
        }
        if (! empty ( $value )) {
            $string = $name . ":" . $value;
        }
        if ($overrideExisting == false) {
            $headers = $this->getHeaders ();
            if (! array_key_exists ( $name, $headers )) {
                header ( $string );
            }
        } else {
            header ( $string );
        }
        return $this;
    }
    /**
     * Remove all headers from stack.
     * Word of warning, this will clear everything and
     * your browser would download the content versus
     * displaying it. At least set the content-type after
     */
    public function clearAllHeaders() {
        header_remove ();
        return $this;
    }
    /**
     * Remove header by the name of $name
     * from the stack
     */
    public function unsetHeader($name) {
        header_remove ( $name );
        return $this;
    }
    /**
     * Assign a content type (application/json etc)
     *
     * @param string $type
     * @return Rest_Response
     */
    public function setContentType($type) {
        $this->setHeader ( "Content-Type", $type . "; charset=UTF-8" );
        return $this;
    }
    /**
     * Manually assign the body content to a response.
     *
     * @param string $content
     * @return Rest_Response
     */
    public function setBody($content) {
        $this->_body = $content;
        return $this;
    }
    /**
     * Retrieve the array of response headers
     * from the stack.
     *
     * @return array
     */
    public function getHeaders() {
        $arh = array ();
        $headers = headers_list ();
        foreach ( $headers as $header ) {
            $header = explode ( ":", $header );
            $arh [array_shift ( $header )] = trim ( implode ( ":", $header ) );
        }
        return $arh;
    }
    /**
     * Retrieve the Content-type from the stack of
     * response headers
     * from the stack.
     *
     * @return array
     */
    public function getContentType() {
        $arh = $this->getHeaders ();
        foreach ( $arh as $k => $v ) {
            if (strtolower ( $k ) == "content-type") {
                return str_replace ( "; charset=UTF-8", "", $v );
            }
        }
        return self::DEFAULT_CONTENT_TYPE;
    }
    /**
     * Set the required headers and send a JSON response
     *
     * @param array $data
     */
    public function sendJsonResponse($data) {
        if (is_array ( $data ) || is_object ( $data )) {
            $final = json_encode ( $data );
        } else {
            $final = $data;
        }
        $this->clearAllHeaders ();
        $this->setContentType ( "application/json" );
        $this->setHeader ( "Content-Length", strlen ( $final ) );
        $this->setBody ( $final );
        $this->sendResponse ();
    }
    /**
     * Immediately send the response, including
     * any headers that need to be sent.
     */
    public function sendResponse() {
        if (empty ( $this->_contentType )) {
            $this->setContentType ( self::DEFAULT_CONTENT_TYPE );
        }
        $body = ob_get_clean ();
        if (! empty ( $this->_body )) {
            $body = $this->_body;
        } else {
            @ob_flush ();
        }
        print (@$body) ;
        @ob_flush ();
        exit;
    }
}
Now that your Request and Response files are complete, we need to add the brains of the operation. Open the file called 'Runner.php' and use this content:
<?php
/**
 * Handles processing and storage of paths.
 *
 * @package Rest_Runner
 * @copyright 2012 Roger E Thomas (http://www.rogerethomas.com)
 * @author Roger Thomas
 *
 */
class Rest_Runner {
    /**
     * Array to hold the core methods relating to paths.
     *
     * @var array
     */
    private $paths = array ();
    /**
     * Path of current request
     *
     * @var string
     */
    private $path = "/";
    /**
     *
     * @var error method, to be called in case of failure.
     */
    private $error = null;
    /**
     *
     * @var Rest_Runner
     */
    protected static $_instance = null;
    /**
     *
     * @return Rest_Runner
     */
    public static function getInstance() {
        if (null == self::$_instance) {
            self::$_instance = new self ();
        }
        return self::$_instance;
    }
    /**
     * Singleton instance
     */
    protected function __construct() {
        $url = "http://" . $_SERVER ['HTTP_HOST'] . $_SERVER ['REQUEST_URI'];
        if (isset ( $_SERVER ['HTTPS'] ) && strtolower ( $_SERVER ['HTTPS'] ) == "on") {
            $url = "https://" . substr ( $url, 7 );
        }
        $parsed = parse_url ( $url );
        if (array_key_exists ( "path", $parsed )) {
            $this->path = $parsed ['path'];
        }
    }
    /**
     *
     * @param string $path
     * @param function $object
     * @return Rest_Runner
     */
    public function get($path, $object) {
        $this->setCall ( $path, $object, "GET" );
        return $this;
    }
    /**
     *
     * @param string $path
     * @param function $object
     * @return Rest_Runner
     */
    public function post($path, $object) {
        $this->setCall ( $path, $object, "POST" );
        return $this;
    }
    /**
     *
     * @param string $path
     * @param function $object
     * @return Rest_Runner
     */
    public function put($path, $object) {
        $this->setCall ( $path, $object, "PUT" );
        return $this;
    }
    /**
     *
     * @param string $path
     * @param function $object
     * @return Rest_Runner
     */
    public function delete($path, $object) {
        $this->setCall ( $path, $object, "DELETE" );
        return $this;
    }
    /**
     *
     * @param string $path
     * @param function $object
     * @return Rest_Runner
     */
    public function catchall($path, $object) {
        $this->setCall ( $path, $object, "ALL" );
        return $this;
    }
    /**
     *
     * @param string $path
     * @param function $object
     */
    private function setCall($path, $object, $method) {
        if (gettype ( $object ) != "object") {
            die ( '$object should be an object' );
        }
        $this->paths [$path . "____" . $method] = $object;
    }
    /**
     *
     * @param string $path
     * @param function $object
     * @return Rest_Runner
     */
    public function error($object) {
        if (gettype ( $object ) != "object") {
            die ( '$object should be an object (function)' );
        }
        $this->error = $object;
        return $this;
    }
    /**
     * Run the application
     */
    public function run() {
        $found = false;
        $hasAll = false;
        foreach ( $this->paths as $k => $v ) {
            if ($k == $this->path . "____" . strtoupper ( $_SERVER ['REQUEST_METHOD'] )) {
                $method = $v;
                $found = true;
                break;
            }
            if ($k == $this->path . "____ALL") {
                $allMethod = $v;
                $hasAll = true;
            }
        }
        if (! $found && ! $hasAll) {
            if (is_null ( $this->error )) {
                header ( $_SERVER ['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500 );
                print ('{"error":"no such method"}') ;
                exit ();
            }
            $e = $this->error;
            $e ();
            exit ();
        }
        if (! $found) {
            $allMethod ();
        } else {
            $method ();
        }
        return;
    }
}
Now, let's revisit that 'index.php' file. We need to create the functions here to control the various request methods and functions to execute if a matching path is found.
Here's a very simple example:
<?php
require("../library/Rest/Runner.php");
require("../library/Rest/Request.php");
require("../library/Rest/Response.php");
Rest_Runner::getInstance()
// Set a method to run in case of an error.
->error(function(){
    die('Error');
})
// This sets all GET requests to /my/path
->get("/my/path",function(){
    $params = Rest_Request::getRequest()->getParams();
    var_dump($params);
    print("GET");
})
// This sets all POST requests to /my/path
->post("/my/path",function(){
    $params = Rest_Request::getRequest()->getParams();
    var_dump($params);
    print("POST");
})
// This sets all PUT requests to /my/path
->put("/my/path",function(){
    $params = Rest_Request::getRequest()->getParams();
    var_dump($params);
    print("PUT");
})
// This sets all DELETE requests to /my/path
->delete("/my/path",function(){
    $params = Rest_Request::getRequest()->getParams();
    var_dump($params);
    print("DELETE");
})
// This sets all requests to /my/path that dont match a previous request method
->catchall("/my/path",function(){
    $params = Rest_Request::getRequest()->getParams();
    var_dump($params);
    print("ALL");
})
->run();
Let's break this file down a bit.
First we tell the service what to do in case of an error. An error would occur if a request is made to the service that doesn't match an exact request method (as provided in other paths) and there isn't a catchall() method.
<?php
Rest_Runner::getInstance()
// Set a method to run in case of an error.
->error(function(){
    die('Error');
})
Next, we provide the Rest_Runner a method to run in case of a HTTP GET Request to the path of '/my/path'
<?php
Rest_Runner::getInstance()
// This sets all GET requests to /my/path
->get("/my/path",function(){
    $params = Rest_Request::getRequest()->getParams();
    var_dump($params);
    print("GET");
})
Next, we provide the Rest_Runner a method to run in case of a HTTP POST Request to the path of '/my/path'
<?php
Rest_Runner::getInstance()
// This sets all POST requests to /my/path
->post("/my/path",function(){
    $params = Rest_Request::getRequest()->getParams();
    var_dump($params);
    print("POST");
})
Lastly, we don't ever really want to use that error() method. So to get around that, we can provide a catchall() method to run in case of an unsupported method being called.
It's up to you how to handle these, but in general you should provide the correct HTTP Response Code. In this case, the HTTP Response Code would be:
'405 Method Not Allowed'
Here's the catchall() method that I've used:
<?php
Rest_Runner::getInstance()
// This sets all requests to /my/path that dont match a previous request method
->catchall("/my/path",function(){
    $params = Rest_Request::getRequest()->getParams();
    var_dump($params);
    print("ALL");
})
Now, you may notice that the line: 'Rest_Runner::getInstance()' is only declared at the top of the file, only once (I've had to put it into each demo piece here due to highlighting restrictions). The Rest_Runner is a singleton instance, so methods return the object so that you can chain up the methods and populate the data you want to provide in one call.
Now, fire it up in your server and try visiting the path '/my/path' in your browser. You should see the idea behind the framework.
I really don't mind you using this code for anything, HOWEVER, please don't remove the copyright from the files, and don't publish it as your own code, or to any code sharing site.
Also, if you do use it and enhance it in any way, please send me the changes (with your changes listed at the top of the file) and I'll publish an update to my blog giving you credit for all changes you make.
